Merge branch 'user-settings' into develop
commit
918bf39169
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Migrations\AbstractMigration;
|
||||
use Phinx\Db\Adapter\MysqlAdapter;
|
||||
|
||||
|
||||
class UserSettings extends AbstractMigration
|
||||
{
|
||||
|
||||
public $autoId = false; // turn off automatic `id` column create. We want it to be `int(10) unsigned`
|
||||
|
||||
|
||||
public function change()
|
||||
{
|
||||
$table = $this->table('user_settings', [
|
||||
'signed' => false,
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
]);
|
||||
$table
|
||||
->addColumn('id', 'integer', [
|
||||
'autoIncrement' => true,
|
||||
'limit' => 10,
|
||||
'signed' => false,
|
||||
])
|
||||
->addPrimaryKey('id')
|
||||
->addColumn('name', 'string', [
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
'limit' => 255,
|
||||
'comment' => 'The name of the user setting',
|
||||
])
|
||||
->addColumn('value', 'text', [
|
||||
'default' => null,
|
||||
'null' => true,
|
||||
'limit' => MysqlAdapter::TEXT_LONG,
|
||||
'comment' => 'The value of the user setting',
|
||||
])
|
||||
->addColumn('user_id', 'integer', [
|
||||
'default' => null,
|
||||
'null' => true,
|
||||
'signed' => false,
|
||||
'length' => 10,
|
||||
])
|
||||
->addColumn('created', 'datetime', [
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('modified', 'datetime', [
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
]);
|
||||
|
||||
$table->addForeignKey('user_id', 'users', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE']);
|
||||
|
||||
$table->addIndex('name')
|
||||
->addIndex('user_id')
|
||||
->addIndex('created')
|
||||
->addIndex('modified');
|
||||
|
||||
$table->create();
|
||||
}
|
||||
}
|
|
@ -128,7 +128,9 @@ class ImporterCommand extends Command
|
|||
$this->loadModel('MetaFields');
|
||||
$entities = [];
|
||||
if (is_null($primary_key)) {
|
||||
$entities = $table->newEntities($data);
|
||||
$entities = $table->newEntities($data, [
|
||||
'accessibleFields' => ($table->newEmptyEntity())->getAccessibleFieldForNew()
|
||||
]);
|
||||
} else {
|
||||
foreach ($data as $i => $item) {
|
||||
$entity = null;
|
||||
|
@ -145,7 +147,9 @@ class ImporterCommand extends Command
|
|||
$this->lockAccess($entity);
|
||||
}
|
||||
if (!is_null($entity)) {
|
||||
$entity = $table->patchEntity($entity, $item);
|
||||
$entity = $table->patchEntity($entity, $item, [
|
||||
'accessibleFields' => $entity->getAccessibleFieldForNew()
|
||||
]);
|
||||
$entities[] = $entity;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"format": "json",
|
||||
"mapping": {
|
||||
"name": "{n}.Organisation.name",
|
||||
"uuid": "{n}.Organisation.uuid",
|
||||
"nationality": "{n}.Organisation.nationality"
|
||||
},
|
||||
"sourceHeaders": {
|
||||
"Authorization": "~~YOUR_API_KEY_HERE~~"
|
||||
}
|
||||
}
|
|
@ -102,7 +102,7 @@ class AppController extends Controller
|
|||
$this->ACL->setPublicInterfaces();
|
||||
if (!empty($this->request->getAttribute('identity'))) {
|
||||
$user = $this->Users->get($this->request->getAttribute('identity')->getIdentifier(), [
|
||||
'contain' => ['Roles', 'Individuals' => 'Organisations']
|
||||
'contain' => ['Roles', 'Individuals' => 'Organisations', 'UserSettings']
|
||||
]);
|
||||
if (!empty($user['disabled'])) {
|
||||
$this->Authentication->logout();
|
||||
|
@ -112,6 +112,8 @@ class AppController extends Controller
|
|||
unset($user['password']);
|
||||
$this->ACL->setUser($user);
|
||||
$this->isAdmin = $user['role']['perm_admin'];
|
||||
$this->set('menu', $this->ACL->getMenu());
|
||||
$this->set('loggedUser', $this->ACL->getUser());
|
||||
} else if ($this->ParamHandler->isRest()) {
|
||||
throw new MethodNotAllowedException(__('Invalid user credentials.'));
|
||||
}
|
||||
|
@ -126,12 +128,16 @@ class AppController extends Controller
|
|||
}
|
||||
|
||||
$this->ACL->checkAccess();
|
||||
$this->set('menu', $this->ACL->getMenu());
|
||||
$this->set('breadcrumb', $this->Navigation->getBreadcrumb());
|
||||
$this->set('ajax', $this->request->is('ajax'));
|
||||
$this->request->getParam('prefix');
|
||||
$this->set('baseurl', Configure::read('App.fullBaseUrl'));
|
||||
|
||||
if (!empty($user) && !empty($user->user_settings_by_name_with_fallback['ui.bsTheme']['value'])) {
|
||||
$this->set('bsTheme', $user->user_settings_by_name_with_fallback['ui.bsTheme']['value']);
|
||||
} else {
|
||||
$this->set('bsTheme', Configure::read('Cerebrate')['ui.bsTheme']);
|
||||
}
|
||||
|
||||
if ($this->modelClass == 'Tags.Tags') {
|
||||
$this->set('metaGroup', !empty($this->isAdmin) ? 'Administration' : 'Cerebrate');
|
||||
|
|
|
@ -457,6 +457,9 @@ class ACLComponent extends Component
|
|||
{
|
||||
$menu = $this->Navigation->getSideMenu();
|
||||
foreach ($menu as $group => $subMenu) {
|
||||
if ($group == '__bookmarks') {
|
||||
continue;
|
||||
}
|
||||
foreach ($subMenu as $subMenuElementName => $subMenuElement) {
|
||||
if (!empty($subMenuElement['url']) && !$this->checkAccessUrl($subMenuElement['url'], true) === true) {
|
||||
unset($menu[$group][$subMenuElementName]);
|
||||
|
|
|
@ -196,6 +196,7 @@ class CRUDComponent extends Component
|
|||
}
|
||||
}
|
||||
}
|
||||
$this->Controller->entity = $data;
|
||||
$this->Controller->set('entity', $data);
|
||||
}
|
||||
|
||||
|
@ -317,6 +318,7 @@ class CRUDComponent extends Component
|
|||
}
|
||||
}
|
||||
}
|
||||
$this->Controller->entity = $data;
|
||||
$this->Controller->set('entity', $data);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class BroodsNavigation extends BaseNavigation
|
||||
{
|
||||
public function addLinks()
|
||||
{
|
||||
$this->bcf->addLink('Broods', 'view', 'LocalTools', 'broodTools');
|
||||
$this->bcf->addLink('Broods', 'edit', 'LocalTools', 'broodTools');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class EncryptionKeysNavigation extends BaseNavigation
|
||||
{
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class InboxNavigation extends BaseNavigation
|
||||
{
|
||||
function addRoutes()
|
||||
{
|
||||
$this->bcf->addRoute('Inbox', 'index', $this->bcf->defaultCRUD('Inbox', 'index'));
|
||||
$this->bcf->addRoute('Inbox', 'view', $this->bcf->defaultCRUD('Inbox', 'view'));
|
||||
$this->bcf->addRoute('Inbox', 'discard', [
|
||||
'label' => __('Discard request'),
|
||||
'icon' => 'trash',
|
||||
'url' => '/inbox/discard/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
]);
|
||||
$this->bcf->addRoute('Inbox', 'process', [
|
||||
'label' => __('Process request'),
|
||||
'icon' => 'cogs',
|
||||
'url' => '/inbox/process/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function addParents()
|
||||
{
|
||||
$this->bcf->addParent('Inbox', 'view', 'Inbox', 'index');
|
||||
$this->bcf->addParent('Inbox', 'discard', 'Inbox', 'index');
|
||||
$this->bcf->addParent('Inbox', 'process', 'Inbox', 'index');
|
||||
}
|
||||
|
||||
public function addLinks()
|
||||
{
|
||||
$this->bcf->addSelfLink('Inbox', 'view');
|
||||
}
|
||||
|
||||
public function addActions()
|
||||
{
|
||||
$this->bcf->addAction('Inbox', 'view', 'Inbox', 'process');
|
||||
$this->bcf->addAction('Inbox', 'view', 'Inbox', 'discard');
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class IndividualsNavigation extends BaseNavigation
|
||||
{
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class InstanceNavigation extends BaseNavigation
|
||||
{
|
||||
function addRoutes()
|
||||
{
|
||||
$this->bcf->addRoute('Instance', 'home', [
|
||||
'label' => __('Home'),
|
||||
'url' => '/',
|
||||
'icon' => 'home'
|
||||
]);
|
||||
$this->bcf->addRoute('Instance', 'settings', [
|
||||
'label' => __('Settings'),
|
||||
'url' => '/instance/settings',
|
||||
'icon' => 'cogs'
|
||||
]);
|
||||
$this->bcf->addRoute('Instance', 'migrationIndex', [
|
||||
'label' => __('Database Migration'),
|
||||
'url' => '/instance/migrationIndex',
|
||||
'icon' => 'database'
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class LocalToolsNavigation extends BaseNavigation
|
||||
{
|
||||
function addRoutes()
|
||||
{
|
||||
$this->bcf->addRoute('LocalTools', 'viewConnector', [
|
||||
'label' => __('View'),
|
||||
'textGetter' => 'connector',
|
||||
'url' => '/localTools/viewConnector/{{connector}}',
|
||||
'url_vars' => ['connector' => 'connector'],
|
||||
]);
|
||||
$this->bcf->addRoute('LocalTools', 'broodTools', [
|
||||
'label' => __('Brood Tools'),
|
||||
'url' => '/localTools/broodTools/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function addParents()
|
||||
{
|
||||
$this->bcf->addParent('LocalTools', 'viewConnector', 'LocalTools', 'index');
|
||||
}
|
||||
|
||||
public function addLinks()
|
||||
{
|
||||
$passedData = $this->request->getParam('pass');
|
||||
if (!empty($passedData[0])) {
|
||||
$brood_id = $passedData[0];
|
||||
$this->bcf->addParent('LocalTools', 'broodTools', 'Broods', 'view', [
|
||||
'textGetter' => [
|
||||
'path' => 'name',
|
||||
'varname' => 'broodEntity',
|
||||
],
|
||||
'url' => "/broods/view/{$brood_id}",
|
||||
]);
|
||||
$this->bcf->addLink('LocalTools', 'broodTools', 'Broods', 'view', [
|
||||
'url' => "/broods/view/{$brood_id}",
|
||||
]);
|
||||
$this->bcf->addLink('LocalTools', 'broodTools', 'Broods', 'edit', [
|
||||
'url' => "/broods/view/{$brood_id}",
|
||||
]);
|
||||
}
|
||||
$this->bcf->addSelfLink('LocalTools', 'broodTools');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class MetaTemplatesNavigation extends BaseNavigation
|
||||
{
|
||||
function addRoutes()
|
||||
{
|
||||
$this->bcf->addRoute('MetaTemplates', 'index', $this->bcf->defaultCRUD('MetaTemplates', 'index'));
|
||||
$this->bcf->addRoute('MetaTemplates', 'view', $this->bcf->defaultCRUD('MetaTemplates', 'view'));
|
||||
$this->bcf->addRoute('MetaTemplates', 'enable', [
|
||||
'label' => __('Enable'),
|
||||
'icon' => 'check',
|
||||
'url' => '/metaTemplates/enable/{{id}}/enabled',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
]);
|
||||
$this->bcf->addRoute('MetaTemplates', 'set_default', [
|
||||
'label' => __('Set as default'),
|
||||
'icon' => 'check',
|
||||
'url' => '/metaTemplates/toggle/{{id}}/default',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function addParents()
|
||||
{
|
||||
$this->bcf->addParent('MetaTemplates', 'view', 'MetaTemplates', 'index');
|
||||
}
|
||||
|
||||
public function addLinks()
|
||||
{
|
||||
$this->bcf->addSelfLink('MetaTemplates', 'view');
|
||||
}
|
||||
|
||||
public function addActions()
|
||||
{
|
||||
$this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'enable');
|
||||
$this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'set_default');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class OrganisationsNavigation extends BaseNavigation
|
||||
{
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class OutboxNavigation extends BaseNavigation
|
||||
{
|
||||
function addRoutes()
|
||||
{
|
||||
$this->bcf->addRoute('Outbox', 'index', $this->bcf->defaultCRUD('Outbox', 'index'));
|
||||
$this->bcf->addRoute('Outbox', 'view', $this->bcf->defaultCRUD('Outbox', 'view'));
|
||||
$this->bcf->addRoute('Outbox', 'discard', [
|
||||
'label' => __('Discard request'),
|
||||
'icon' => 'trash',
|
||||
'url' => '/outbox/discard/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
]);
|
||||
$this->bcf->addRoute('Outbox', 'process', [
|
||||
'label' => __('Process request'),
|
||||
'icon' => 'cogs',
|
||||
'url' => '/outbox/process/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function addParents()
|
||||
{
|
||||
$this->bcf->addParent('Outbox', 'view', 'Outbox', 'index');
|
||||
$this->bcf->addParent('Outbox', 'discard', 'Outbox', 'index');
|
||||
$this->bcf->addParent('Outbox', 'process', 'Outbox', 'index');
|
||||
}
|
||||
|
||||
public function addLinks()
|
||||
{
|
||||
$this->bcf->addSelfLink('Outbox', 'view');
|
||||
}
|
||||
|
||||
public function addActions()
|
||||
{
|
||||
$this->bcf->addAction('Outbox', 'view', 'Outbox', 'process');
|
||||
$this->bcf->addAction('Outbox', 'view', 'Outbox', 'discard');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class RolesNavigation extends BaseNavigation
|
||||
{
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class SharingGroupsNavigation extends BaseNavigation
|
||||
{
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class TagsNavigation extends BaseNavigation
|
||||
{
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class UserSettingsNavigation extends BaseNavigation
|
||||
{
|
||||
public function addLinks()
|
||||
{
|
||||
$bcf = $this->bcf;
|
||||
$request = $this->request;
|
||||
$this->bcf->addLink('UserSettings', 'index', 'Users', 'view', function ($config) use ($bcf, $request) {
|
||||
if (!empty($request->getQuery('Users_id'))) {
|
||||
$user_id = h($request->getQuery('Users_id'));
|
||||
$linkData = [
|
||||
'label' => __('View user [{0}]', h($user_id)),
|
||||
'url' => sprintf('/users/view/%s', h($user_id))
|
||||
];
|
||||
return $linkData;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
$this->bcf->addLink('UserSettings', 'index', 'Users', 'edit', function ($config) use ($bcf, $request) {
|
||||
if (!empty($request->getQuery('Users_id'))) {
|
||||
$user_id = h($request->getQuery('Users_id'));
|
||||
$linkData = [
|
||||
'label' => __('Edit user [{0}]', h($user_id)),
|
||||
'url' => sprintf('/users/edit/%s', h($user_id))
|
||||
];
|
||||
return $linkData;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (!empty($request->getQuery('Users_id'))) {
|
||||
$this->bcf->addSelfLink('UserSettings', 'index');
|
||||
}
|
||||
if ($this->request->getParam('controller') == 'UserSettings' && $this->request->getParam('action') == 'index') {
|
||||
if (!empty($this->request->getQuery('Users_id'))) {
|
||||
$user_id = $this->request->getQuery('Users_id');
|
||||
$this->bcf->addParent('UserSettings', 'index', 'Users', 'view', [
|
||||
'textGetter' => [
|
||||
'path' => 'username',
|
||||
'varname' => 'settingsForUser',
|
||||
],
|
||||
'url' => "/users/view/{$user_id}"
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class UsersNavigation extends BaseNavigation
|
||||
{
|
||||
public function addRoutes()
|
||||
{
|
||||
$this->bcf->addRoute('Users', 'settings', [
|
||||
'label' => __('User settings'),
|
||||
'url' => '/users/settings/',
|
||||
'icon' => 'user-cog'
|
||||
]);
|
||||
}
|
||||
|
||||
public function addParents()
|
||||
{
|
||||
// $this->bcf->addParent('Users', 'settings', 'Users', 'view');
|
||||
}
|
||||
|
||||
public function addLinks()
|
||||
{
|
||||
$bcf = $this->bcf;
|
||||
$request = $this->request;
|
||||
$passedData = $this->request->getParam('pass');
|
||||
$this->bcf->addLink('Users', 'view', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) {
|
||||
if (!empty($passedData[0])) {
|
||||
$user_id = $passedData[0];
|
||||
$linkData = [
|
||||
'label' => __('Account settings', h($user_id)),
|
||||
'url' => sprintf('/users/settings/%s', h($user_id))
|
||||
];
|
||||
return $linkData;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
$this->bcf->addLink('Users', 'view', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) {
|
||||
if (!empty($passedData[0])) {
|
||||
$user_id = $passedData[0];
|
||||
$linkData = [
|
||||
'label' => __('User Setting [{0}]', h($user_id)),
|
||||
'url' => sprintf('/user-settings/index?Users.id=%s', h($user_id))
|
||||
];
|
||||
return $linkData;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
$this->bcf->addLink('Users', 'edit', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) {
|
||||
if (!empty($passedData[0])) {
|
||||
$user_id = $passedData[0];
|
||||
$linkData = [
|
||||
'label' => __('Account settings', h($user_id)),
|
||||
'url' => sprintf('/users/settings/%s', h($user_id))
|
||||
];
|
||||
return $linkData;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
$this->bcf->addLink('Users', 'edit', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) {
|
||||
if (!empty($passedData[0])) {
|
||||
$user_id = $passedData[0];
|
||||
$linkData = [
|
||||
'label' => __('User Setting [{0}]', h($user_id)),
|
||||
'url' => sprintf('/user-settings/index?Users.id=%s', h($user_id))
|
||||
];
|
||||
return $linkData;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
$this->bcf->addLink('Users', 'settings', 'Users', 'view', function ($config) use ($bcf, $request, $passedData) {
|
||||
if (!empty($passedData[0])) {
|
||||
$user_id = $passedData[0];
|
||||
$linkData = [
|
||||
'label' => __('View user', h($user_id)),
|
||||
'url' => sprintf('/users/view/%s', h($user_id))
|
||||
];
|
||||
return $linkData;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
$this->bcf->addSelfLink('Users', 'settings', [
|
||||
'label' => __('Account settings')
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
class BaseNavigation
|
||||
{
|
||||
protected $bcf;
|
||||
protected $request;
|
||||
|
||||
public function __construct($bcf, $request)
|
||||
{
|
||||
$this->bcf = $bcf;
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function addRoutes() {}
|
||||
public function addParents() {}
|
||||
public function addLinks() {}
|
||||
public function addActions() {}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
namespace SidemenuNavigation;
|
||||
|
||||
use Cake\Core\Configure;
|
||||
|
||||
class Sidemenu {
|
||||
private $iconTable;
|
||||
private $request;
|
||||
|
||||
public function __construct($iconTable, $request)
|
||||
{
|
||||
$this->iconTable = $iconTable;
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function get(): array
|
||||
{
|
||||
return [
|
||||
__('ContactDB') => [
|
||||
'Individuals' => [
|
||||
'label' => __('Individuals'),
|
||||
'icon' => $this->iconTable['Individuals'],
|
||||
'url' => '/individuals/index',
|
||||
],
|
||||
'Organisations' => [
|
||||
'label' => __('Organisations'),
|
||||
'icon' => $this->iconTable['Organisations'],
|
||||
'url' => '/organisations/index',
|
||||
],
|
||||
'EncryptionKeys' => [
|
||||
'label' => __('Encryption keys'),
|
||||
'icon' => $this->iconTable['EncryptionKeys'],
|
||||
'url' => '/encryptionKeys/index',
|
||||
]
|
||||
],
|
||||
__('Trust Circles') => [
|
||||
'SharingGroups' => [
|
||||
'label' => __('Sharing Groups'),
|
||||
'icon' => $this->iconTable['SharingGroups'],
|
||||
'url' => '/sharingGroups/index',
|
||||
]
|
||||
],
|
||||
__('Synchronisation') => [
|
||||
'Broods' => [
|
||||
'label' => __('Broods'),
|
||||
'icon' => $this->iconTable['Broods'],
|
||||
'url' => '/broods/index',
|
||||
]
|
||||
],
|
||||
__('Administration') => [
|
||||
'Roles' => [
|
||||
'label' => __('Roles'),
|
||||
'icon' => $this->iconTable['Roles'],
|
||||
'url' => '/roles/index',
|
||||
],
|
||||
'Users' => [
|
||||
'label' => __('Users'),
|
||||
'icon' => $this->iconTable['Users'],
|
||||
'url' => '/users/index',
|
||||
],
|
||||
'UserSettings' => [
|
||||
'label' => __('Users Settings'),
|
||||
'icon' => $this->iconTable['UserSettings'],
|
||||
'url' => '/user-settings/index',
|
||||
],
|
||||
'Messages' => [
|
||||
'label' => __('Messages'),
|
||||
'icon' => $this->iconTable['Inbox'],
|
||||
'url' => '/inbox/index',
|
||||
'children' => [
|
||||
'index' => [
|
||||
'url' => '/inbox/index',
|
||||
'label' => __('Inbox')
|
||||
],
|
||||
'outbox' => [
|
||||
'url' => '/outbox/index',
|
||||
'label' => __('Outbox')
|
||||
],
|
||||
]
|
||||
],
|
||||
'Add-ons' => [
|
||||
'label' => __('Add-ons'),
|
||||
'icon' => 'puzzle-piece',
|
||||
'children' => [
|
||||
'MetaTemplates.index' => [
|
||||
'label' => __('Meta Field Templates'),
|
||||
'icon' => $this->iconTable['MetaTemplates'],
|
||||
'url' => '/metaTemplates/index',
|
||||
],
|
||||
'LocalTools.index' => [
|
||||
'label' => __('Local Tools'),
|
||||
'icon' => $this->iconTable['LocalTools'],
|
||||
'url' => '/localTools/index',
|
||||
],
|
||||
'Tags.index' => [
|
||||
'label' => __('Tags'),
|
||||
'icon' => $this->iconTable['Tags'],
|
||||
'url' => '/tags/index',
|
||||
],
|
||||
]
|
||||
],
|
||||
'Instance' => [
|
||||
'label' => __('Instance'),
|
||||
'icon' => $this->iconTable['Instance'],
|
||||
'children' => [
|
||||
'Settings' => [
|
||||
'label' => __('Settings'),
|
||||
'url' => '/instance/settings',
|
||||
'icon' => 'cogs',
|
||||
],
|
||||
'Database' => [
|
||||
'label' => __('Database'),
|
||||
'url' => '/instance/migrationIndex',
|
||||
'icon' => 'database',
|
||||
],
|
||||
]
|
||||
],
|
||||
],
|
||||
'Open' => [
|
||||
'Organisations' => [
|
||||
'label' => __('Organisations'),
|
||||
'icon' => $this->iconTable['Organisations'],
|
||||
'url' => '/open/organisations/index',
|
||||
'children' => [
|
||||
'index' => [
|
||||
'url' => '/open/organisations/index',
|
||||
'label' => __('List organisations')
|
||||
],
|
||||
],
|
||||
'open' => in_array('organisations', Configure::read('Cerebrate.open'))
|
||||
],
|
||||
'Individuals' => [
|
||||
'label' => __('Individuals'),
|
||||
'icon' => $this->iconTable['Individuals'],
|
||||
'url' => '/open/individuals/index',
|
||||
'children' => [
|
||||
'index' => [
|
||||
'url' => '/open/individuals/index',
|
||||
'label' => __('List individuals')
|
||||
],
|
||||
],
|
||||
'open' => in_array('individuals', Configure::read('Cerebrate.open'))
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
|
@ -4,10 +4,16 @@ namespace App\Controller\Component;
|
|||
|
||||
use Cake\Controller\Component;
|
||||
use Cake\Core\Configure;
|
||||
use Cake\Core\App;
|
||||
use Cake\Utility\Inflector;
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\Filesystem\Folder;
|
||||
use Cake\Routing\Router;
|
||||
use Cake\ORM\TableRegistry;
|
||||
use Exception;
|
||||
|
||||
use SidemenuNavigation\Sidemenu;
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'sidemenu.php');
|
||||
|
||||
class NavigationComponent extends Component
|
||||
{
|
||||
|
@ -21,6 +27,7 @@ class NavigationComponent extends Component
|
|||
'Broods' => 'network-wired',
|
||||
'Roles' => 'id-badge',
|
||||
'Users' => 'users',
|
||||
'UserSettings' => 'user-cog',
|
||||
'Inbox' => 'inbox',
|
||||
'Outbox' => 'inbox',
|
||||
'MetaTemplates' => 'object-group',
|
||||
|
@ -42,389 +49,340 @@ class NavigationComponent extends Component
|
|||
|
||||
public function getSideMenu(): array
|
||||
{
|
||||
$sidemenu = new Sidemenu($this->iconToTableMapping, $this->request);
|
||||
$sidemenu = $sidemenu->get();
|
||||
$sidemenu = $this->addUserBookmarks($sidemenu);
|
||||
return $sidemenu;
|
||||
}
|
||||
|
||||
|
||||
public function addUserBookmarks($sidemenu): array
|
||||
{
|
||||
$bookmarks = $this->getUserBookmarks();
|
||||
$sidemenu = array_merge([
|
||||
'__bookmarks' => $bookmarks
|
||||
], $sidemenu);
|
||||
return $sidemenu;
|
||||
}
|
||||
|
||||
public function getUserBookmarks(): array
|
||||
{
|
||||
$userSettingTable = TableRegistry::getTableLocator()->get('UserSettings');
|
||||
$setting = $userSettingTable->getSettingByName($this->request->getAttribute('identity'), 'ui.bookmarks');
|
||||
$bookmarks = is_null($setting) ? [] : json_decode($setting->value, true);
|
||||
|
||||
$links = array_map(function($bookmark) {
|
||||
return [
|
||||
'ContactDB' => [
|
||||
'Individuals' => [
|
||||
'label' => __('Individuals'),
|
||||
'icon' => $this->iconToTableMapping['Individuals'],
|
||||
'url' => '/individuals/index',
|
||||
],
|
||||
'Organisations' => [
|
||||
'label' => __('Organisations'),
|
||||
'icon' => $this->iconToTableMapping['Organisations'],
|
||||
'url' => '/organisations/index',
|
||||
],
|
||||
'EncryptionKeys' => [
|
||||
'label' => __('Encryption keys'),
|
||||
'icon' => $this->iconToTableMapping['EncryptionKeys'],
|
||||
'url' => '/encryptionKeys/index',
|
||||
]
|
||||
],
|
||||
'Trust Circles' => [
|
||||
'SharingGroups' => [
|
||||
'label' => __('Sharing Groups'),
|
||||
'icon' => $this->iconToTableMapping['SharingGroups'],
|
||||
'url' => '/sharingGroups/index',
|
||||
]
|
||||
],
|
||||
'Sync' => [
|
||||
'Broods' => [
|
||||
'label' => __('Broods'),
|
||||
'icon' => $this->iconToTableMapping['Broods'],
|
||||
'url' => '/broods/index',
|
||||
]
|
||||
],
|
||||
'Administration' => [
|
||||
'Roles' => [
|
||||
'label' => __('Roles'),
|
||||
'icon' => $this->iconToTableMapping['Roles'],
|
||||
'url' => '/roles/index',
|
||||
],
|
||||
'Users' => [
|
||||
'label' => __('Users'),
|
||||
'icon' => $this->iconToTableMapping['Users'],
|
||||
'url' => '/users/index',
|
||||
],
|
||||
'Messages' => [
|
||||
'label' => __('Messages'),
|
||||
'icon' => $this->iconToTableMapping['Inbox'],
|
||||
'url' => '/inbox/index',
|
||||
'children' => [
|
||||
'index' => [
|
||||
'url' => '/inbox/index',
|
||||
'label' => __('Inbox')
|
||||
],
|
||||
'outbox' => [
|
||||
'url' => '/outbox/index',
|
||||
'label' => __('Outbox')
|
||||
],
|
||||
]
|
||||
],
|
||||
'Add-ons' => [
|
||||
'label' => __('Add-ons'),
|
||||
'icon' => 'puzzle-piece',
|
||||
'children' => [
|
||||
'MetaTemplates.index' => [
|
||||
'label' => __('Meta Field Templates'),
|
||||
'icon' => $this->iconToTableMapping['MetaTemplates'],
|
||||
'url' => '/metaTemplates/index',
|
||||
],
|
||||
'LocalTools.index' => [
|
||||
'label' => __('Local Tools'),
|
||||
'icon' => $this->iconToTableMapping['LocalTools'],
|
||||
'url' => '/localTools/index',
|
||||
],
|
||||
'Tags.index' => [
|
||||
'label' => __('Tags'),
|
||||
'icon' => $this->iconToTableMapping['Tags'],
|
||||
'url' => '/tags/index',
|
||||
],
|
||||
]
|
||||
],
|
||||
'Instance' => [
|
||||
'label' => __('Instance'),
|
||||
'icon' => $this->iconToTableMapping['Instance'],
|
||||
'children' => [
|
||||
'Settings' => [
|
||||
'label' => __('Settings'),
|
||||
'url' => '/instance/settings',
|
||||
'icon' => 'cogs',
|
||||
],
|
||||
'Database' => [
|
||||
'label' => __('Database'),
|
||||
'url' => '/instance/migrationIndex',
|
||||
'icon' => 'database',
|
||||
],
|
||||
]
|
||||
],
|
||||
],
|
||||
'Open' => [
|
||||
'Organisations' => [
|
||||
'label' => __('Organisations'),
|
||||
'icon' => $this->iconToTableMapping['Organisations'],
|
||||
'url' => '/open/organisations/index',
|
||||
'children' => [
|
||||
'index' => [
|
||||
'url' => '/open/organisations/index',
|
||||
'label' => __('List organisations')
|
||||
],
|
||||
],
|
||||
'open' => in_array('organisations', Configure::read('Cerebrate.open'))
|
||||
],
|
||||
'Individuals' => [
|
||||
'label' => __('Individuals'),
|
||||
'icon' => $this->iconToTableMapping['Individuals'],
|
||||
'url' => '/open/individuals/index',
|
||||
'children' => [
|
||||
'index' => [
|
||||
'url' => '/open/individuals/index',
|
||||
'label' => __('List individuals')
|
||||
],
|
||||
],
|
||||
'open' => in_array('individuals', Configure::read('Cerebrate.open'))
|
||||
]
|
||||
]
|
||||
'name' => $bookmark['name'],
|
||||
'label' => $bookmark['label'],
|
||||
'url' => $bookmark['url'],
|
||||
];
|
||||
}, $bookmarks);
|
||||
return $links;
|
||||
}
|
||||
|
||||
public function getBreadcrumb(): array
|
||||
{
|
||||
$controller = $this->request->getParam('controller');
|
||||
$action = $this->request->getParam('action');
|
||||
if (empty($this->fullBreadcrumb[$controller]['routes']["{$controller}:{$action}"])) {
|
||||
if (empty($this->fullBreadcrumb[$controller][$action])) {
|
||||
return [[
|
||||
'label' => $controller,
|
||||
'url' => Router::url(['controller' => $controller, 'action' => $action]),
|
||||
]]; // no breadcrumb defined for this endpoint
|
||||
}
|
||||
$currentRoute = $this->fullBreadcrumb[$controller]['routes']["{$controller}:{$action}"];
|
||||
$breadcrumbPath = $this->getBreadcrumbPath("{$controller}:{$action}", $currentRoute);
|
||||
return $breadcrumbPath['objects'];
|
||||
$currentRoute = $this->fullBreadcrumb[$controller][$action];
|
||||
$breadcrumbPath = $this->getBreadcrumbPath($currentRoute);
|
||||
return $breadcrumbPath;
|
||||
}
|
||||
|
||||
public function getBreadcrumbPath(string $startRoute, array $currentRoute): array
|
||||
public function getBreadcrumbPath(array $currentRoute): array
|
||||
{
|
||||
$route = $startRoute;
|
||||
$path = [
|
||||
'routes' => [],
|
||||
'objects' => [],
|
||||
];
|
||||
$visited = [];
|
||||
while (empty($visited[$route])) {
|
||||
$visited[$route] = true;
|
||||
$path['routes'][] = $route;
|
||||
$path['objects'][] = $currentRoute;
|
||||
$path = [];
|
||||
$visitedURL = [];
|
||||
while (empty($visitedURL[$currentRoute['url']])) {
|
||||
$visitedURL[$currentRoute['url']] = true;
|
||||
$path[] = $currentRoute;
|
||||
if (!empty($currentRoute['after'])) {
|
||||
if (is_callable($currentRoute['after'])) {
|
||||
$route = $currentRoute['after']();
|
||||
} else {
|
||||
$route = $currentRoute['after'];
|
||||
$split = explode(':', $currentRoute['after']);
|
||||
$currentRoute = $this->fullBreadcrumb[$split[0]]['routes'][$currentRoute['after']];
|
||||
}
|
||||
if (empty($route)) {
|
||||
continue;
|
||||
}
|
||||
$currentRoute = $route;
|
||||
}
|
||||
}
|
||||
$path['routes'] = array_reverse($path['routes']);
|
||||
$path['objects'] = array_reverse($path['objects']);
|
||||
$path = array_reverse($path);
|
||||
return $path;
|
||||
}
|
||||
|
||||
private function insertInheritance(array $config, array $fullConfig): array
|
||||
{
|
||||
if (!empty($config['routes'])) {
|
||||
foreach ($config['routes'] as $routeName => $value) {
|
||||
$config['routes'][$routeName]['route_path'] = $routeName;
|
||||
if (!empty($value['inherit'])) {
|
||||
$default = $config['defaults'][$value['inherit']] ?? [];
|
||||
$config['routes'][$routeName] = array_merge($config['routes'][$routeName], $default);
|
||||
unset($config['routes'][$routeName]['inherit']);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
private function insertRelated(array $config, array $fullConfig): array
|
||||
{
|
||||
if (!empty($config['routes'])) {
|
||||
foreach ($config['routes'] as $routeName => $value) {
|
||||
if (!empty($value['links'])) {
|
||||
foreach ($value['links'] as $i => $linkedRoute) {
|
||||
$split = explode(':', $linkedRoute);
|
||||
if (!empty($fullConfig[$split[0]]['routes'][$linkedRoute])) {
|
||||
$linkedRouteObject = $fullConfig[$split[0]]['routes'][$linkedRoute];
|
||||
if (!empty($linkedRouteObject)) {
|
||||
$config['routes'][$routeName]['links'][$i] = $linkedRouteObject;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
unset($config['routes'][$routeName]['links'][$i]);
|
||||
}
|
||||
}
|
||||
if (!empty($value['actions'])) {
|
||||
foreach ($value['actions'] as $i => $linkedRoute) {
|
||||
$split = explode(':', $linkedRoute);
|
||||
if (!empty($fullConfig[$split[0]]['routes'][$linkedRoute])) {
|
||||
$linkedRouteObject = $fullConfig[$split[0]]['routes'][$linkedRoute];
|
||||
if (!empty($linkedRouteObject)) {
|
||||
$config['routes'][$routeName]['actions'][$i] = $linkedRouteObject;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
unset($config['routes'][$routeName]['actions'][$i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function getDefaultCRUDConfig(string $controller, array $overrides=[], array $merges=[]): array
|
||||
{
|
||||
$table = TableRegistry::getTableLocator()->get($controller);
|
||||
$default = [
|
||||
'defaults' => [
|
||||
'depth-1' => [
|
||||
'after' => "{$controller}:index",
|
||||
'textGetter' => !empty($table->getDisplayField()) ? $table->getDisplayField() : 'id',
|
||||
'links' => [
|
||||
"{$controller}:view",
|
||||
"{$controller}:edit",
|
||||
],
|
||||
'actions' => [
|
||||
"{$controller}:delete",
|
||||
],
|
||||
]
|
||||
],
|
||||
'routes' => [
|
||||
"{$controller}:index" => [
|
||||
'label' => Inflector::humanize($controller),
|
||||
'url' => "/{$controller}/index",
|
||||
'icon' => $this->iconToTableMapping[$controller]
|
||||
],
|
||||
"{$controller}:view" => [
|
||||
'label' => __('View'),
|
||||
'icon' => 'eye',
|
||||
'inherit' => 'depth-1',
|
||||
'url' => "/{$controller}/view/{{id}}",
|
||||
'url_vars' => ['id' => 'id'],
|
||||
],
|
||||
"{$controller}:edit" => [
|
||||
'label' => __('Edit'),
|
||||
'icon' => 'edit',
|
||||
'inherit' => 'depth-1',
|
||||
'url' => "/{$controller}/edit/{{id}}",
|
||||
'url_vars' => ['id' => 'id'],
|
||||
],
|
||||
"{$controller}:delete" => [
|
||||
'label' => __('Delete'),
|
||||
'icon' => 'trash',
|
||||
'inherit' => 'depth-1',
|
||||
'url' => "/{$controller}/delete/{{id}}",
|
||||
'url_vars' => ['id' => 'id'],
|
||||
],
|
||||
]
|
||||
];
|
||||
$merged = array_merge_recursive($default, $merges);
|
||||
$overridden = array_replace_recursive($merged, $overrides);
|
||||
return $overridden;
|
||||
}
|
||||
|
||||
public function genBreadcrumb(): array
|
||||
{
|
||||
$fullConfig = [
|
||||
'Individuals' => $this->getDefaultCRUDConfig('Individuals'),
|
||||
'Organisations' => $this->getDefaultCRUDConfig('Organisations'),
|
||||
'EncryptionKeys' => $this->getDefaultCRUDConfig('EncryptionKeys'),
|
||||
'SharingGroups' => $this->getDefaultCRUDConfig('SharingGroups'),
|
||||
'Broods' => $this->getDefaultCRUDConfig('Broods', [], [
|
||||
'defaults' => ['depth-1' => ['links' => 'LocalTools:brood_tools']]
|
||||
]),
|
||||
'Roles' => $this->getDefaultCRUDConfig('Roles'),
|
||||
'Users' => $this->getDefaultCRUDConfig('Users'),
|
||||
'Inbox' => $this->getDefaultCRUDConfig('Inbox', [
|
||||
'defaults' => ['depth-1' => [
|
||||
'links' => ['Inbox:view', 'Inbox:process'],
|
||||
'actions' => ['Inbox:process', 'Inbox:delete'],
|
||||
]]
|
||||
], [
|
||||
'routes' => [
|
||||
'Inbox:discard' => [
|
||||
'label' => __('Discard request'),
|
||||
'inherit' => 'depth-1',
|
||||
'url' => '/inbox/discard/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
],
|
||||
'Inbox:process' => [
|
||||
'label' => __('Process request'),
|
||||
'inherit' => 'depth-1',
|
||||
'url' => '/inbox/process/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
],
|
||||
]
|
||||
]),
|
||||
'Outbox' => $this->getDefaultCRUDConfig('Outbox', [
|
||||
'defaults' => ['depth-1' => [
|
||||
'links' => ['Outbox:view', 'Outbox:process'],
|
||||
'actions' => ['Outbox:process', 'Outbox:delete'],
|
||||
]]
|
||||
], [
|
||||
'routes' => [
|
||||
'Outbox:discard' => [
|
||||
'label' => __('Discard request'),
|
||||
'inherit' => 'depth-1',
|
||||
'url' => '/outbox/discard/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
],
|
||||
'Outbox:process' => [
|
||||
'label' => __('Process request'),
|
||||
'inherit' => 'depth-1',
|
||||
'url' => '/outbox/process/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
],
|
||||
]
|
||||
]),
|
||||
'MetaTemplates' => $this->getDefaultCRUDConfig('MetaTemplates', [
|
||||
'defaults' => ['depth-1' => [
|
||||
'links' => ['MetaTemplates:view', ''], // '' to remove leftovers. Related to https://www.php.net/manual/en/function.array-replace-recursive.php#124705
|
||||
'actions' => ['MetaTemplates:toggle'],
|
||||
]]
|
||||
], [
|
||||
'routes' => [
|
||||
'MetaTemplates:toggle' => [
|
||||
'label' => __('Toggle Meta-template'),
|
||||
'inherit' => 'depth-1',
|
||||
'url' => '/MetaTemplates/toggle/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
],
|
||||
]
|
||||
]),
|
||||
'Tags' => $this->getDefaultCRUDConfig('Tags', [
|
||||
'defaults' => ['depth-1' => ['textGetter' => 'name']]
|
||||
]),
|
||||
'LocalTools' => [
|
||||
'routes' => [
|
||||
'LocalTools:index' => [
|
||||
'label' => __('Local Tools'),
|
||||
'url' => '/localTools/index',
|
||||
'icon' => $this->iconToTableMapping['LocalTools'],
|
||||
],
|
||||
'LocalTools:viewConnector' => [
|
||||
'label' => __('View'),
|
||||
'textGetter' => 'name',
|
||||
'url' => '/localTools/viewConnector/{{connector}}',
|
||||
'url_vars' => ['connector' => 'connector'],
|
||||
'after' => 'LocalTools:index',
|
||||
],
|
||||
'LocalTools:broodTools' => [
|
||||
'label' => __('Brood Tools'),
|
||||
'url' => '/localTools/broodTools/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
],
|
||||
]
|
||||
],
|
||||
'Instance' => [
|
||||
'routes' => [
|
||||
'Instance:home' => [
|
||||
'label' => __('Home'),
|
||||
'url' => '/',
|
||||
'icon' => 'home'
|
||||
],
|
||||
'Instance:settings' => [
|
||||
'label' => __('Settings'),
|
||||
'url' => '/instance/settings',
|
||||
'icon' => 'cogs'
|
||||
],
|
||||
'Instance:migrationIndex' => [
|
||||
'label' => __('Database Migration'),
|
||||
'url' => '/instance/migrationIndex',
|
||||
'icon' => 'database'
|
||||
],
|
||||
]
|
||||
]
|
||||
];
|
||||
foreach ($fullConfig as $controller => $config) {
|
||||
$fullConfig[$controller] = $this->insertInheritance($config, $fullConfig);
|
||||
}
|
||||
foreach ($fullConfig as $controller => $config) {
|
||||
$fullConfig[$controller] = $this->insertRelated($config, $fullConfig);
|
||||
}
|
||||
$request = $this->request;
|
||||
$bcf = new BreadcrumbFactory($this->iconToTableMapping);
|
||||
$fullConfig = $this->getFullConfig($bcf, $this->request);
|
||||
return $fullConfig;
|
||||
}
|
||||
|
||||
private function loadNavigationClasses($bcf, $request)
|
||||
{
|
||||
$navigationClasses = [];
|
||||
$navigationDir = new Folder(APP . DS . 'Controller' . DS . 'Component' . DS . 'Navigation');
|
||||
$navigationFiles = $navigationDir->find('.*\.php', true);
|
||||
foreach ($navigationFiles as $navigationFile) {
|
||||
if ($navigationFile == 'base.php' || $navigationFile == 'sidemenu.php') {
|
||||
continue;
|
||||
}
|
||||
$navigationClassname = str_replace('.php', '', $navigationFile);
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . $navigationFile);
|
||||
$reflection = new \ReflectionClass("BreadcrumbNavigation\\{$navigationClassname}Navigation");
|
||||
$navigationClasses[$navigationClassname] = $reflection->newInstance($bcf, $request);
|
||||
}
|
||||
return $navigationClasses;
|
||||
}
|
||||
|
||||
public function getFullConfig($bcf, $request)
|
||||
{
|
||||
$navigationClasses = $this->loadNavigationClasses($bcf, $request);
|
||||
$CRUDControllers = [
|
||||
'Individuals',
|
||||
'Organisations',
|
||||
'EncryptionKeys',
|
||||
'SharingGroups',
|
||||
'Broods',
|
||||
'Roles',
|
||||
'Users',
|
||||
'Tags',
|
||||
'LocalTools',
|
||||
'UserSettings',
|
||||
];
|
||||
foreach ($CRUDControllers as $controller) {
|
||||
$bcf->setDefaultCRUDForModel($controller);
|
||||
}
|
||||
|
||||
foreach ($navigationClasses as $className => $class) {
|
||||
$class->addRoutes();
|
||||
}
|
||||
foreach ($navigationClasses as $className => $class) {
|
||||
$class->addParents();
|
||||
}
|
||||
foreach ($navigationClasses as $className => $class) {
|
||||
$class->addLinks();
|
||||
}
|
||||
foreach ($navigationClasses as $className => $class) {
|
||||
$class->addActions();
|
||||
}
|
||||
return $bcf->getEndpoints();
|
||||
}
|
||||
}
|
||||
|
||||
class BreadcrumbFactory
|
||||
{
|
||||
private $endpoints = [];
|
||||
private $iconToTableMapping = [];
|
||||
|
||||
public function __construct($iconToTableMapping)
|
||||
{
|
||||
$this->iconToTableMapping = $iconToTableMapping;
|
||||
}
|
||||
|
||||
public function defaultCRUD(string $controller, string $action, array $overrides = []): array
|
||||
{
|
||||
$table = TableRegistry::getTableLocator()->get($controller);
|
||||
$item = [];
|
||||
if ($action === 'index') {
|
||||
$item = $this->genRouteConfig($controller, $action, [
|
||||
'label' => __('{0} index', Inflector::humanize($controller)),
|
||||
'url' => "/{$controller}/index",
|
||||
'icon' => $this->iconToTableMapping[$controller]
|
||||
]);
|
||||
} else if ($action === 'view') {
|
||||
$item = $this->genRouteConfig($controller, $action, [
|
||||
'label' => __('View'),
|
||||
'icon' => 'eye',
|
||||
'url' => "/{$controller}/view/{{id}}",
|
||||
'url_vars' => ['id' => 'id'],
|
||||
'textGetter' => !empty($table->getDisplayField()) ? $table->getDisplayField() : 'id',
|
||||
]);
|
||||
} else if ($action === 'add') {
|
||||
$item = $this->genRouteConfig($controller, $action, [
|
||||
'label' => __('[new {0}]', $controller),
|
||||
'icon' => 'plus',
|
||||
'url' => "/{$controller}/add",
|
||||
]);
|
||||
} else if ($action === 'edit') {
|
||||
$item = $this->genRouteConfig($controller, $action, [
|
||||
'label' => __('Edit'),
|
||||
'icon' => 'edit',
|
||||
'url' => "/{$controller}/edit/{{id}}",
|
||||
'url_vars' => ['id' => 'id'],
|
||||
'textGetter' => !empty($table->getDisplayField()) ? $table->getDisplayField() : 'id',
|
||||
]);
|
||||
} else if ($action === 'delete') {
|
||||
$item = $this->genRouteConfig($controller, $action, [
|
||||
'label' => __('Delete'),
|
||||
'icon' => 'trash',
|
||||
'url' => "/{$controller}/delete/{{id}}",
|
||||
'url_vars' => ['id' => 'id'],
|
||||
'textGetter' => !empty($table->getDisplayField()) ? $table->getDisplayField() : 'id',
|
||||
]);
|
||||
}
|
||||
$item['route_path'] = "{$controller}:{$action}";
|
||||
$item = array_merge($item, $overrides);
|
||||
return $item;
|
||||
}
|
||||
|
||||
public function genRouteConfig($controller, $action, $config = [])
|
||||
{
|
||||
$routeConfig = [
|
||||
'controller' => $controller,
|
||||
'action' => $action,
|
||||
'route_path' => "{$controller}:{$action}",
|
||||
];
|
||||
$routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'url');
|
||||
$routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'url_vars');
|
||||
$routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'icon');
|
||||
$routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'label');
|
||||
$routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'textGetter');
|
||||
return $routeConfig;
|
||||
}
|
||||
|
||||
private function addIfNotEmpty($arr, $data, $key, $default = null)
|
||||
{
|
||||
if (!empty($data[$key])) {
|
||||
$arr[$key] = $data[$key];
|
||||
} else {
|
||||
if (!is_null($default)) {
|
||||
$arr[$key] = $default;
|
||||
}
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
|
||||
public function addRoute($controller, $action, $config = []) {
|
||||
$this->endpoints[$controller][$action] = $this->genRouteConfig($controller, $action, $config);
|
||||
}
|
||||
|
||||
public function setDefaultCRUDForModel($controller)
|
||||
{
|
||||
$this->addRoute($controller, 'index', $this->defaultCRUD($controller, 'index'));
|
||||
$this->addRoute($controller, 'view', $this->defaultCRUD($controller, 'view'));
|
||||
$this->addRoute($controller, 'add', $this->defaultCRUD($controller, 'add'));
|
||||
$this->addRoute($controller, 'edit', $this->defaultCRUD($controller, 'edit'));
|
||||
$this->addRoute($controller, 'delete', $this->defaultCRUD($controller, 'delete'));
|
||||
|
||||
$this->addParent($controller, 'view', $controller, 'index');
|
||||
$this->addParent($controller, 'add', $controller, 'index');
|
||||
$this->addParent($controller, 'edit', $controller, 'index');
|
||||
$this->addParent($controller, 'delete', $controller, 'index');
|
||||
|
||||
$this->addSelfLink($controller, 'view');
|
||||
$this->addLink($controller, 'view', $controller, 'edit');
|
||||
$this->addLink($controller, 'edit', $controller, 'view');
|
||||
$this->addSelfLink($controller, 'edit');
|
||||
|
||||
$this->addAction($controller, 'view', $controller, 'add');
|
||||
$this->addAction($controller, 'view', $controller, 'delete');
|
||||
$this->addAction($controller, 'edit', $controller, 'add');
|
||||
$this->addAction($controller, 'edit', $controller, 'delete');
|
||||
}
|
||||
|
||||
public function get($controller, $action)
|
||||
{
|
||||
if (empty($this->endpoints[$controller]) || empty($this->endpoints[$controller][$action])) {
|
||||
throw new \Exception(sprintf("Tried to add a reference to %s:%s which does not exists", $controller, $action), 1);
|
||||
}
|
||||
return $this->endpoints[$controller][$action];
|
||||
}
|
||||
|
||||
public function getEndpoints()
|
||||
{
|
||||
return $this->endpoints;
|
||||
}
|
||||
|
||||
public function addParent(string $sourceController, string $sourceAction, string $targetController, string $targetAction, $overrides = [])
|
||||
{
|
||||
$routeSourceConfig = $this->get($sourceController, $sourceAction);
|
||||
$routeTargetConfig = $this->get($targetController, $targetAction);
|
||||
$overrides = $this->execClosureIfNeeded($overrides, $routeSourceConfig);
|
||||
if (!is_array($overrides)) {
|
||||
throw new \Exception(sprintf("Override closure for %s:%s -> %s:%s must return an array", $sourceController, $sourceAction, $targetController, $targetAction), 1);
|
||||
}
|
||||
$routeTargetConfig = array_merge($routeTargetConfig, $overrides);
|
||||
$parents = array_merge($routeSourceConfig['after'] ?? [], $routeTargetConfig);
|
||||
$this->endpoints[$sourceController][$sourceAction]['after'] = $parents;
|
||||
}
|
||||
|
||||
public function addSelfLink(string $controller, string $action, array $options=[])
|
||||
{
|
||||
$this->addLink($controller, $action, $controller, $action, array_merge($options, [
|
||||
'selfLink' => true,
|
||||
]));
|
||||
}
|
||||
|
||||
public function addLink(string $sourceController, string $sourceAction, string $targetController, string $targetAction, $overrides = [])
|
||||
{
|
||||
$routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true);
|
||||
$routeTargetConfig = $this->getRouteConfig($targetController, $targetAction);
|
||||
$overrides = $this->execClosureIfNeeded($overrides, $routeSourceConfig);
|
||||
if (is_null($overrides)) {
|
||||
// Overrides is null, the link should not be added
|
||||
return;
|
||||
}
|
||||
if (!is_array($overrides)) {
|
||||
throw new \Exception(sprintf("Override closure for %s:%s -> %s:%s must return an array", $sourceController, $sourceAction, $targetController, $targetAction), 1);
|
||||
}
|
||||
$routeTargetConfig = array_merge($routeTargetConfig, $overrides);
|
||||
$links = array_merge($routeSourceConfig['links'] ?? [], [$routeTargetConfig]);
|
||||
$this->endpoints[$sourceController][$sourceAction]['links'] = $links;
|
||||
}
|
||||
|
||||
public function addAction(string $sourceController, string $sourceAction, string $targetController, string $targetAction, $overrides = [])
|
||||
{
|
||||
$routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true);
|
||||
$routeTargetConfig = $this->getRouteConfig($targetController, $targetAction);
|
||||
$overrides = $this->execClosureIfNeeded($overrides, $routeSourceConfig);
|
||||
if (!is_array($overrides)) {
|
||||
throw new \Exception(sprintf("Override closure for %s:%s -> %s:%s must return an array", $sourceController, $sourceAction, $targetController, $targetAction), 1);
|
||||
}
|
||||
$routeTargetConfig = array_merge($routeTargetConfig, $overrides);
|
||||
$links = array_merge($routeSourceConfig['actions'] ?? [], [$routeTargetConfig]);
|
||||
$this->endpoints[$sourceController][$sourceAction]['actions'] = $links;
|
||||
}
|
||||
|
||||
public function removeLink(string $sourceController, string $sourceAction, string $targetController, string $targetAction)
|
||||
{
|
||||
$routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true);
|
||||
if (!empty($routeSourceConfig['links'])) {
|
||||
foreach ($routeSourceConfig['links'] as $i => $routeConfig) {
|
||||
if ($routeConfig['controller'] == $targetController && $routeConfig['action'] == $targetAction) {
|
||||
unset($routeSourceConfig['links'][$i]);
|
||||
$this->endpoints[$sourceController][$sourceAction]['links'] = $routeSourceConfig['links'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getRouteConfig($controller, $action, $fullRoute = false)
|
||||
{
|
||||
$routeConfig = $this->get($controller, $action);
|
||||
if (empty($fullRoute)) {
|
||||
unset($routeConfig['after']);
|
||||
unset($routeConfig['links']);
|
||||
unset($routeConfig['actions']);
|
||||
}
|
||||
return $routeConfig;
|
||||
}
|
||||
|
||||
private function execClosureIfNeeded($closure, $routeConfig=[])
|
||||
{
|
||||
if (is_callable($closure)) {
|
||||
return $closure($routeConfig);
|
||||
}
|
||||
return $closure;
|
||||
}
|
||||
}
|
|
@ -280,6 +280,8 @@ class LocalToolsController extends AppController
|
|||
return $this->RestResponse->viewData($tools, 'json');
|
||||
}
|
||||
$this->set('id', $id);
|
||||
$brood = $this->Broods->get($id);
|
||||
$this->set('broodEntity', $brood);
|
||||
$this->set('data', $tools);
|
||||
$this->set('metaGroup', 'Administration');
|
||||
}
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Controller\AppController;
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\Utility\Text;
|
||||
use \Cake\Database\Expression\QueryExpression;
|
||||
use Cake\Http\Exception\NotFoundException;
|
||||
use Cake\Http\Exception\MethodNotAllowedException;
|
||||
use Cake\Http\Exception\ForbiddenException;
|
||||
|
||||
class UserSettingsController extends AppController
|
||||
{
|
||||
public $quickFilterFields = [['name' => true], ['value' => true]];
|
||||
public $filterFields = ['name', 'value', 'Users.id'];
|
||||
public $containFields = ['Users'];
|
||||
|
||||
public function index()
|
||||
{
|
||||
$conditions = [];
|
||||
$this->CRUD->index([
|
||||
'conditions' => [],
|
||||
'contain' => $this->containFields,
|
||||
'filters' => $this->filterFields,
|
||||
'quickFilters' => $this->quickFilterFields,
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
if (!empty($this->request->getQuery('Users_id'))) {
|
||||
$settingsForUser = $this->UserSettings->Users->find()->where([
|
||||
'id' => $this->request->getQuery('Users_id')
|
||||
])->first();
|
||||
$this->set('settingsForUser', $settingsForUser);
|
||||
}
|
||||
}
|
||||
|
||||
public function view($id)
|
||||
{
|
||||
$this->CRUD->view($id, [
|
||||
'contain' => ['Users']
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
|
||||
public function add($user_id = false)
|
||||
{
|
||||
$this->CRUD->add([
|
||||
'redirect' => ['action' => 'index', $user_id],
|
||||
'beforeSave' => function($data) use ($user_id) {
|
||||
$data['user_id'] = $user_id;
|
||||
return $data;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$dropdownData = [
|
||||
'user' => $this->UserSettings->Users->find('list', [
|
||||
'sort' => ['username' => 'asc']
|
||||
]),
|
||||
];
|
||||
$this->set(compact('dropdownData'));
|
||||
$this->set('user_id', $user_id);
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$entity = $this->UserSettings->find()->where([
|
||||
'id' => $id
|
||||
])->first();
|
||||
$entity = $this->CRUD->edit($id, [
|
||||
'redirect' => ['action' => 'index', $entity->user_id]
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$dropdownData = [
|
||||
'user' => $this->UserSettings->Users->find('list', [
|
||||
'sort' => ['username' => 'asc']
|
||||
]),
|
||||
];
|
||||
$this->set(compact('dropdownData'));
|
||||
$this->set('user_id', $this->entity->user_id);
|
||||
$this->render('add');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$this->CRUD->delete($id);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSettingByName($settingsName)
|
||||
{
|
||||
$setting = $this->UserSettings->getSettingByName($this->ACL->getUser(), $settingsName);
|
||||
if (is_null($setting)) {
|
||||
throw new NotFoundException(__('Invalid {0} for user {1}.', __('User setting'), $this->ACL->getUser()->username));
|
||||
}
|
||||
$this->CRUD->view($setting->id, [
|
||||
'contain' => ['Users']
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->render('view');
|
||||
}
|
||||
|
||||
public function setSetting($settingsName = false)
|
||||
{
|
||||
if (!$this->request->is('get')) {
|
||||
$setting = $this->UserSettings->getSettingByName($this->ACL->getUser(), $settingsName);
|
||||
if (is_null($setting)) { // setting not found, create it
|
||||
$result = $this->UserSettings->createSetting($this->ACL->getUser(), $settingsName, $this->request->getData()['value']);
|
||||
} else {
|
||||
$result = $this->UserSettings->editSetting($this->ACL->getUser(), $settingsName, $this->request->getData()['value']);
|
||||
}
|
||||
$success = !empty($result);
|
||||
$message = $success ? __('Setting saved') : __('Could not save setting');
|
||||
$this->CRUD->setResponseForController('setSetting', $success, $message, $result);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
$this->set('settingName', $settingsName);
|
||||
}
|
||||
|
||||
public function saveSetting()
|
||||
{
|
||||
if ($this->request->is('post')) {
|
||||
$data = $this->ParamHandler->harvestParams([
|
||||
'name',
|
||||
'value'
|
||||
]);
|
||||
$setting = $this->UserSettings->getSettingByName($this->ACL->getUser(), $data['name']);
|
||||
if (is_null($setting)) { // setting not found, create it
|
||||
$result = $this->UserSettings->createSetting($this->ACL->getUser(), $data['name'], $data['value']);
|
||||
} else {
|
||||
$result = $this->UserSettings->editSetting($this->ACL->getUser(), $data['name'], $data['value']);
|
||||
}
|
||||
$success = !empty($result);
|
||||
$message = $success ? __('Setting saved') : __('Could not save setting');
|
||||
$this->CRUD->setResponseForController('setSetting', $success, $message, $result);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getBookmarks($forSidebar=false)
|
||||
{
|
||||
$bookmarks = $this->UserSettings->getSettingByName($this->ACL->getUser(), $this->UserSettings->BOOKMARK_SETTING_NAME);
|
||||
$bookmarks = json_decode($bookmarks['value'], true);
|
||||
$this->set('user_id', $this->ACL->getUser()->id);
|
||||
$this->set('bookmarks', $bookmarks);
|
||||
$this->set('forSidebar', $forSidebar);
|
||||
$this->render('/element/UserSettings/saved-bookmarks');
|
||||
}
|
||||
|
||||
public function saveBookmark()
|
||||
{
|
||||
if (!$this->request->is('get')) {
|
||||
$result = $this->UserSettings->saveBookmark($this->ACL->getUser(), $this->request->getData());
|
||||
$success = !empty($result);
|
||||
$message = $success ? __('Bookmark saved') : __('Could not save bookmark');
|
||||
$this->CRUD->setResponseForController('saveBookmark', $success, $message, $result);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
$this->set('user_id', $this->ACL->getUser()->id);
|
||||
}
|
||||
|
||||
public function deleteBookmark()
|
||||
{
|
||||
if (!$this->request->is('get')) {
|
||||
$result = $this->UserSettings->deleteBookmark($this->ACL->getUser(), $this->request->getData());
|
||||
$success = !empty($result);
|
||||
$message = $success ? __('Bookmark deleted') : __('Could not delete bookmark');
|
||||
$this->CRUD->setResponseForController('deleteBookmark', $success, $message, $result);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
$this->set('user_id', $this->ACL->getUser()->id);
|
||||
}
|
||||
|
||||
}
|
|
@ -11,7 +11,7 @@ class UsersController extends AppController
|
|||
{
|
||||
public $filterFields = ['Individuals.uuid', 'username', 'Individuals.email', 'Individuals.first_name', 'Individuals.last_name'];
|
||||
public $quickFilterFields = ['Individuals.uuid', ['username' => true], ['Individuals.first_name' => true], ['Individuals.last_name' => true], 'Individuals.email'];
|
||||
public $containFields = ['Individuals', 'Roles'];
|
||||
public $containFields = ['Individuals', 'Roles', 'UserSettings'];
|
||||
|
||||
public function index()
|
||||
{
|
||||
|
@ -148,6 +148,16 @@ class UsersController extends AppController
|
|||
}
|
||||
}
|
||||
|
||||
public function settings()
|
||||
{
|
||||
$this->set('user', $this->ACL->getUser());
|
||||
$all = $this->Users->UserSettings->getSettingsFromProviderForUser($this->ACL->getUser()['id'], true);
|
||||
$this->set('settingsProvider', $all['settingsProvider']);
|
||||
$this->set('settings', $all['settings']);
|
||||
$this->set('settingsFlattened', $all['settingsFlattened']);
|
||||
$this->set('notices', $all['notices']);
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
$this->InboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors');
|
||||
|
|
|
@ -16,4 +16,14 @@ class Individual extends AppModel
|
|||
protected $_accessibleOnNew = [
|
||||
'uuid' => true,
|
||||
];
|
||||
|
||||
protected $_virtual = ['full_name'];
|
||||
|
||||
protected function _getFullName()
|
||||
{
|
||||
if (empty($this->first_name) && empty($this->last_name)) {
|
||||
return $this->username;
|
||||
}
|
||||
return sprintf("%s %s", $this->first_name, $this->last_name);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,42 @@ use App\Model\Entity\AppModel;
|
|||
use Cake\ORM\Entity;
|
||||
use Authentication\PasswordHasher\DefaultPasswordHasher;
|
||||
|
||||
require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'UserSettingsProvider.php');
|
||||
use App\Settings\SettingsProvider\UserSettingsProvider;
|
||||
|
||||
class User extends AppModel
|
||||
{
|
||||
protected $_hidden = ['password', 'confirm_password'];
|
||||
|
||||
protected $_virtual = ['user_settings_by_name', 'user_settings_by_name_with_fallback'];
|
||||
|
||||
protected function _getUserSettingsByName()
|
||||
{
|
||||
$settingsByName = [];
|
||||
if (!empty($this->user_settings)) {
|
||||
foreach ($this->user_settings as $i => $setting) {
|
||||
$settingsByName[$setting->name] = $setting;
|
||||
}
|
||||
}
|
||||
return $settingsByName;
|
||||
}
|
||||
|
||||
protected function _getUserSettingsByNameWithFallback()
|
||||
{
|
||||
if (!isset($this->SettingsProvider)) {
|
||||
$this->SettingsProvider = new UserSettingsProvider();
|
||||
}
|
||||
$settingsByNameWithFallback = [];
|
||||
if (!empty($this->user_settings)) {
|
||||
foreach ($this->user_settings as $i => $setting) {
|
||||
$settingsByNameWithFallback[$setting->name] = $setting->value;
|
||||
}
|
||||
}
|
||||
$settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settingsByNameWithFallback);
|
||||
$settingsFlattened = $this->SettingsProvider->flattenSettingsConfiguration($settingsProvider);
|
||||
return $settingsFlattened;
|
||||
}
|
||||
|
||||
protected function _setPassword(string $password) : ?string
|
||||
{
|
||||
if (strlen($password) > 0) {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Entity;
|
||||
|
||||
use App\Model\Entity\AppModel;
|
||||
|
||||
class UserSetting extends AppModel
|
||||
{
|
||||
}
|
|
@ -1,28 +1,28 @@
|
|||
<?php
|
||||
namespace App\Model\Table;
|
||||
namespace App\Settings\SettingsProvider;
|
||||
|
||||
use App\Model\Table\AppTable;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
class SettingsProviderTable extends AppTable
|
||||
class BaseSettingsProvider
|
||||
{
|
||||
private $settingsConfiguration = [];
|
||||
private $error_critical = '',
|
||||
protected $settingsConfiguration = [];
|
||||
protected $error_critical = '',
|
||||
$error_warning = '',
|
||||
$error_info = '';
|
||||
private $severities = ['info', 'warning', 'critical'];
|
||||
protected $severities = ['info', 'warning', 'critical'];
|
||||
|
||||
public function initialize(array $config): void
|
||||
public function __construct()
|
||||
{
|
||||
parent::initialize($config);
|
||||
$this->settingsConfiguration = $this->generateSettingsConfiguration();
|
||||
$this->setTable(false);
|
||||
$this->error_critical = __('Cerebrate will not operate correctly or will be unsecure until these issues are resolved.');
|
||||
$this->error_warning = __('Some of the features of Cerebrate cannot be utilised until these issues are resolved.');
|
||||
$this->error_info = __('There are some optional tweaks that could be done to improve the looks of your Cerebrate instance.');
|
||||
if (!isset($this->settingValidator)) {
|
||||
$this->settingValidator = new SettingValidator();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supports up to 3 levels:
|
||||
|
@ -46,151 +46,9 @@ class SettingsProviderTable extends AppTable
|
|||
* redacted [optional]: Should the setting value be redacted. FIXME: To implement
|
||||
* cli_only [optional]: Should this setting be modified only via the CLI.
|
||||
*/
|
||||
private function generateSettingsConfiguration()
|
||||
protected function generateSettingsConfiguration()
|
||||
{
|
||||
return [
|
||||
'Application' => [
|
||||
'General' => [
|
||||
'Essentials' => [
|
||||
'_description' => __('Ensentials settings required for the application to run normally.'),
|
||||
'_icon' => 'user-cog',
|
||||
'app.baseurl' => [
|
||||
'name' => __('Base URL'),
|
||||
'type' => 'string',
|
||||
'description' => __('The base url of the application (in the format https://www.mymispinstance.com or https://myserver.com/misp). Several features depend on this setting being correctly set to function.'),
|
||||
'default' => '',
|
||||
'severity' => 'critical',
|
||||
'test' => 'testBaseURL',
|
||||
],
|
||||
'app.uuid' => [
|
||||
'name' => 'UUID',
|
||||
'type' => 'string',
|
||||
'description' => __('The Cerebrate instance UUID. This UUID is used to identify this instance.'),
|
||||
'default' => '',
|
||||
'severity' => 'critical',
|
||||
'test' => 'testUuid',
|
||||
],
|
||||
],
|
||||
'Miscellaneous' => [
|
||||
'sc2.hero' => [
|
||||
'description' => 'The true hero',
|
||||
'default' => 'Sarah Kerrigan',
|
||||
'name' => 'Hero',
|
||||
'options' => [
|
||||
'Jim Raynor' => 'Jim Raynor',
|
||||
'Sarah Kerrigan' => 'Sarah Kerrigan',
|
||||
'Artanis' => 'Artanis',
|
||||
'Zeratul' => 'Zeratul',
|
||||
],
|
||||
'type' => 'select'
|
||||
],
|
||||
'sc2.antagonists' => [
|
||||
'description' => 'The bad guys',
|
||||
'default' => 'Amon',
|
||||
'name' => 'Antagonists',
|
||||
'options' => function($settingsProviders) {
|
||||
return [
|
||||
'Amon' => 'Amon',
|
||||
'Sarah Kerrigan' => 'Sarah Kerrigan',
|
||||
'Narud' => 'Narud',
|
||||
];
|
||||
},
|
||||
'severity' => 'warning',
|
||||
'type' => 'multi-select'
|
||||
],
|
||||
],
|
||||
'floating-setting' => [
|
||||
'description' => 'floaringSetting',
|
||||
// 'default' => 'A default value',
|
||||
'name' => 'Uncategorized Setting',
|
||||
// 'severity' => 'critical',
|
||||
'severity' => 'warning',
|
||||
// 'severity' => 'info',
|
||||
'type' => 'integer'
|
||||
],
|
||||
],
|
||||
'Network' => [
|
||||
'Proxy' => [
|
||||
'proxy.host' => [
|
||||
'name' => __('Host'),
|
||||
'type' => 'string',
|
||||
'description' => __('The hostname of an HTTP proxy for outgoing sync requests. Leave empty to not use a proxy.'),
|
||||
'test' => 'testHostname',
|
||||
],
|
||||
'proxy.port' => [
|
||||
'name' => __('Port'),
|
||||
'type' => 'integer',
|
||||
'description' => __('The TCP port for the HTTP proxy.'),
|
||||
'test' => 'testForRangeXY',
|
||||
],
|
||||
'proxy.user' => [
|
||||
'name' => __('User'),
|
||||
'type' => 'string',
|
||||
'description' => __('The authentication username for the HTTP proxy.'),
|
||||
'default' => 'admin',
|
||||
'dependsOn' => 'proxy.host',
|
||||
],
|
||||
'proxy.password' => [
|
||||
'name' => __('Password'),
|
||||
'type' => 'string',
|
||||
'description' => __('The authentication password for the HTTP proxy.'),
|
||||
'default' => '',
|
||||
'dependsOn' => 'proxy.host',
|
||||
],
|
||||
],
|
||||
],
|
||||
'UI' => [
|
||||
'General' => [
|
||||
'ui.bsTheme' => [
|
||||
'description' => 'The Bootstrap theme to use for the application',
|
||||
'default' => 'default',
|
||||
'name' => 'UI Theme',
|
||||
'options' => function($settingsProviders) {
|
||||
$instanceTable = TableRegistry::getTableLocator()->get('Instance');
|
||||
$themes = $instanceTable->getAvailableThemes();
|
||||
return array_combine($themes, $themes);
|
||||
},
|
||||
'severity' => 'info',
|
||||
'type' => 'select'
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'Security' => [
|
||||
'Development' => [
|
||||
'Debugging' => [
|
||||
'security.debug' => [
|
||||
'name' => __('Debug Level'),
|
||||
'type' => 'select',
|
||||
'description' => __('The debug level of the instance'),
|
||||
'default' => 0,
|
||||
'options' => [
|
||||
0 => __('Debug Off'),
|
||||
1 => __('Debug On'),
|
||||
2 => __('Debug On + SQL Dump'),
|
||||
],
|
||||
'test' => function($value, $setting, $validator) {
|
||||
$validator->range('value', [0, 3]);
|
||||
return testValidator($value, $validator);
|
||||
},
|
||||
],
|
||||
],
|
||||
]
|
||||
],
|
||||
'Features' => [
|
||||
'Demo Settings' => [
|
||||
'demo.switch' => [
|
||||
'name' => __('Switch'),
|
||||
'type' => 'boolean',
|
||||
'description' => __('A switch acting as a checkbox'),
|
||||
'default' => false,
|
||||
'test' => function() {
|
||||
return 'Fake error';
|
||||
},
|
||||
],
|
||||
]
|
||||
],
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -214,7 +72,7 @@ class SettingsProviderTable extends AppTable
|
|||
* @param array $settings the settings
|
||||
* @return void
|
||||
*/
|
||||
private function mergeSettingsIntoSettingConfiguration(array $settingConf, array $settings, string $path=''): array
|
||||
protected function mergeSettingsIntoSettingConfiguration(array $settingConf, array $settings, string $path=''): array
|
||||
{
|
||||
foreach ($settingConf as $key => $value) {
|
||||
if ($this->isSettingMetaKey($key)) {
|
||||
|
@ -277,12 +135,12 @@ class SettingsProviderTable extends AppTable
|
|||
return $notices;
|
||||
}
|
||||
|
||||
private function isLeaf($setting)
|
||||
protected function isLeaf($setting)
|
||||
{
|
||||
return !empty($setting['name']) && !empty($setting['type']);
|
||||
}
|
||||
|
||||
private function evaluateLeaf($setting, $settingSection)
|
||||
protected function evaluateLeaf($setting, $settingSection)
|
||||
{
|
||||
$skipValidation = false;
|
||||
if ($setting['type'] == 'select' || $setting['type'] == 'multi-select') {
|
||||
|
@ -353,12 +211,6 @@ class SettingsProviderTable extends AppTable
|
|||
}
|
||||
}
|
||||
|
||||
function testValidator($value, $validator)
|
||||
{
|
||||
$errors = $validator->validate(['value' => $value]);
|
||||
return !empty($errors) ? implode(', ', $errors['value']) : true;
|
||||
}
|
||||
|
||||
class SettingValidator
|
||||
{
|
||||
|
||||
|
@ -384,22 +236,4 @@ class SettingValidator
|
|||
{
|
||||
return !empty($value) ? true : __('Cannot be empty');
|
||||
}
|
||||
|
||||
public function testBaseURL($value, &$setting)
|
||||
{
|
||||
if (empty($value)) {
|
||||
return __('Cannot be empty');
|
||||
}
|
||||
if (!empty($value) && !preg_match('/^http(s)?:\/\//i', $value)) {
|
||||
return __('Invalid URL, please make sure that the protocol is set.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function testUuid($value, &$setting) {
|
||||
if (empty($value) || !preg_match('/^\{?[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}\}?$/', $value)) {
|
||||
return __('Invalid UUID.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
<?php
|
||||
|
||||
namespace App\Settings\SettingsProvider;
|
||||
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'BaseSettingsProvider.php');
|
||||
|
||||
use App\Settings\SettingsProvider\BaseSettingsProvider;
|
||||
use App\Settings\SettingsProvider\SettingValidator;
|
||||
|
||||
class CerebrateSettingsProvider extends BaseSettingsProvider
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->settingValidator = new CerebrateSettingValidator();
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function generateSettingsConfiguration()
|
||||
{
|
||||
return [
|
||||
'Application' => [
|
||||
'General' => [
|
||||
'Essentials' => [
|
||||
'_description' => __('Ensentials settings required for the application to run normally.'),
|
||||
'_icon' => 'user-cog',
|
||||
'app.baseurl' => [
|
||||
'name' => __('Base URL'),
|
||||
'type' => 'string',
|
||||
'description' => __('The base url of the application (in the format https://www.mymispinstance.com or https://myserver.com/misp). Several features depend on this setting being correctly set to function.'),
|
||||
'default' => '',
|
||||
'severity' => 'critical',
|
||||
'test' => 'testBaseURL',
|
||||
],
|
||||
'app.uuid' => [
|
||||
'name' => 'UUID',
|
||||
'type' => 'string',
|
||||
'description' => __('The Cerebrate instance UUID. This UUID is used to identify this instance.'),
|
||||
'default' => '',
|
||||
'severity' => 'critical',
|
||||
'test' => 'testUuid',
|
||||
],
|
||||
],
|
||||
'Miscellaneous' => [
|
||||
'sc2.hero' => [
|
||||
'description' => 'The true hero',
|
||||
'default' => 'Sarah Kerrigan',
|
||||
'name' => 'Hero',
|
||||
'options' => [
|
||||
'Jim Raynor' => 'Jim Raynor',
|
||||
'Sarah Kerrigan' => 'Sarah Kerrigan',
|
||||
'Artanis' => 'Artanis',
|
||||
'Zeratul' => 'Zeratul',
|
||||
],
|
||||
'type' => 'select'
|
||||
],
|
||||
'sc2.antagonists' => [
|
||||
'description' => 'The bad guys',
|
||||
'default' => 'Amon',
|
||||
'name' => 'Antagonists',
|
||||
'options' => function ($settingsProviders) {
|
||||
return [
|
||||
'Amon' => 'Amon',
|
||||
'Sarah Kerrigan' => 'Sarah Kerrigan',
|
||||
'Narud' => 'Narud',
|
||||
];
|
||||
},
|
||||
'severity' => 'warning',
|
||||
'type' => 'multi-select'
|
||||
],
|
||||
],
|
||||
'floating-setting' => [
|
||||
'description' => 'floaringSetting',
|
||||
// 'default' => 'A default value',
|
||||
'name' => 'Uncategorized Setting',
|
||||
// 'severity' => 'critical',
|
||||
'severity' => 'warning',
|
||||
// 'severity' => 'info',
|
||||
'type' => 'integer'
|
||||
],
|
||||
],
|
||||
'Network' => [
|
||||
'Proxy' => [
|
||||
'proxy.host' => [
|
||||
'name' => __('Host'),
|
||||
'type' => 'string',
|
||||
'description' => __('The hostname of an HTTP proxy for outgoing sync requests. Leave empty to not use a proxy.'),
|
||||
'test' => 'testHostname',
|
||||
],
|
||||
'proxy.port' => [
|
||||
'name' => __('Port'),
|
||||
'type' => 'integer',
|
||||
'description' => __('The TCP port for the HTTP proxy.'),
|
||||
'test' => 'testForRangeXY',
|
||||
],
|
||||
'proxy.user' => [
|
||||
'name' => __('User'),
|
||||
'type' => 'string',
|
||||
'description' => __('The authentication username for the HTTP proxy.'),
|
||||
'default' => 'admin',
|
||||
'dependsOn' => 'proxy.host',
|
||||
],
|
||||
'proxy.password' => [
|
||||
'name' => __('Password'),
|
||||
'type' => 'string',
|
||||
'description' => __('The authentication password for the HTTP proxy.'),
|
||||
'default' => '',
|
||||
'dependsOn' => 'proxy.host',
|
||||
],
|
||||
],
|
||||
],
|
||||
'UI' => [
|
||||
'General' => [
|
||||
'ui.bsTheme' => [
|
||||
'description' => 'The Bootstrap theme to use for the application',
|
||||
'default' => 'default',
|
||||
'name' => 'UI Theme',
|
||||
'options' => function ($settingsProviders) {
|
||||
$instanceTable = TableRegistry::getTableLocator()->get('Instance');
|
||||
$themes = $instanceTable->getAvailableThemes();
|
||||
return array_combine($themes, $themes);
|
||||
},
|
||||
'severity' => 'info',
|
||||
'type' => 'select'
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'Security' => [
|
||||
'Development' => [
|
||||
'Debugging' => [
|
||||
'security.debug' => [
|
||||
'name' => __('Debug Level'),
|
||||
'type' => 'select',
|
||||
'description' => __('The debug level of the instance'),
|
||||
'default' => 0,
|
||||
'options' => [
|
||||
0 => __('Debug Off'),
|
||||
1 => __('Debug On'),
|
||||
2 => __('Debug On + SQL Dump'),
|
||||
],
|
||||
'test' => function ($value, $setting, $validator) {
|
||||
$validator->range('value', [0, 3]);
|
||||
return testValidator($value, $validator);
|
||||
},
|
||||
],
|
||||
],
|
||||
]
|
||||
],
|
||||
'Features' => [
|
||||
'Demo Settings' => [
|
||||
'demo.switch' => [
|
||||
'name' => __('Switch'),
|
||||
'type' => 'boolean',
|
||||
'description' => __('A switch acting as a checkbox'),
|
||||
'default' => false,
|
||||
'test' => function () {
|
||||
return 'Fake error';
|
||||
},
|
||||
],
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
function testValidator($value, $validator)
|
||||
{
|
||||
$errors = $validator->validate(['value' => $value]);
|
||||
return !empty($errors) ? implode(', ', $errors['value']) : true;
|
||||
}
|
||||
|
||||
class CerebrateSettingValidator extends SettingValidator
|
||||
{
|
||||
public function testUuid($value, &$setting)
|
||||
{
|
||||
if (empty($value) || !preg_match('/^\{?[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}\}?$/', $value)) {
|
||||
return __('Invalid UUID.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function testBaseURL($value, &$setting)
|
||||
{
|
||||
if (empty($value)) {
|
||||
return __('Cannot be empty');
|
||||
}
|
||||
if (!empty($value) && !preg_match('/^http(s)?:\/\//i', $value)) {
|
||||
return __('Invalid URL, please make sure that the protocol is set.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Settings\SettingsProvider;
|
||||
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'BaseSettingsProvider.php');
|
||||
|
||||
use App\Settings\SettingsProvider\BaseSettingsProvider;
|
||||
|
||||
class UserSettingsProvider extends BaseSettingsProvider
|
||||
{
|
||||
protected function generateSettingsConfiguration()
|
||||
{
|
||||
return [
|
||||
__('Appearance') => [
|
||||
__('User Interface') => [
|
||||
'ui.bsTheme' => [
|
||||
'description' => 'The Bootstrap theme to use for the application',
|
||||
'default' => 'default',
|
||||
'name' => 'UI Theme',
|
||||
'options' => (function () {
|
||||
$instanceTable = TableRegistry::getTableLocator()->get('Instance');
|
||||
$themes = $instanceTable->getAvailableThemes();
|
||||
return array_combine($themes, $themes);
|
||||
})(),
|
||||
'severity' => 'info',
|
||||
'type' => 'select'
|
||||
],
|
||||
'ui.sidebar.expanded' => [
|
||||
'name' => __('Sidebar expanded'),
|
||||
'type' => 'boolean',
|
||||
'description' => __('Should the left navigation sidebar expanded and locked.'),
|
||||
'default' => false,
|
||||
'severity' => 'info',
|
||||
],
|
||||
'ui.sidebar.include_bookmarks' => [
|
||||
'name' => __('Include bookmarks in the sidebar'),
|
||||
'type' => 'boolean',
|
||||
'description' => __('Should bookmarks links included in the sidebar.'),
|
||||
'default' => false,
|
||||
'severity' => 'info',
|
||||
],
|
||||
]
|
||||
],
|
||||
__('Account Security') => [
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
|
@ -3,9 +3,10 @@ namespace App\Model\Table;
|
|||
|
||||
use App\Model\Table\AppTable;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\Core\Configure;
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'CerebrateSettingsProvider.php');
|
||||
use App\Settings\SettingsProvider\CerebrateSettingsProvider;
|
||||
|
||||
class SettingsTable extends AppTable
|
||||
{
|
||||
|
@ -16,7 +17,7 @@ class SettingsTable extends AppTable
|
|||
{
|
||||
parent::initialize($config);
|
||||
$this->setTable(false);
|
||||
$this->SettingsProvider = TableRegistry::getTableLocator()->get('SettingsProvider');
|
||||
$this->SettingsProvider = new CerebrateSettingsProvider();
|
||||
}
|
||||
|
||||
public function getSettings($full=false): array
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Table;
|
||||
|
||||
use App\Model\Table\AppTable;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\Validation\Validator;
|
||||
|
||||
require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'UserSettingsProvider.php');
|
||||
use App\Settings\SettingsProvider\UserSettingsProvider;
|
||||
|
||||
class UserSettingsTable extends AppTable
|
||||
{
|
||||
protected $BOOKMARK_SETTING_NAME = 'ui.bookmarks';
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
$this->addBehavior('Timestamp');
|
||||
$this->belongsTo(
|
||||
'Users'
|
||||
);
|
||||
$this->setDisplayField('name');
|
||||
|
||||
$this->SettingsProvider = new UserSettingsProvider();
|
||||
}
|
||||
|
||||
public function validationDefault(Validator $validator): Validator
|
||||
{
|
||||
$validator
|
||||
->requirePresence(['name', 'user_id'], 'create')
|
||||
->notEmptyString('name', __('Please fill this field'))
|
||||
->notEmptyString('user_id', __('Please supply the user id to which this setting belongs to'));
|
||||
return $validator;
|
||||
}
|
||||
|
||||
public function getSettingsFromProviderForUser($user_id, $full = false): array
|
||||
{
|
||||
$settingsTmp = $this->getSettingsForUser($user_id)->toArray();
|
||||
$settings = [];
|
||||
foreach ($settingsTmp as $setting) {
|
||||
$settings[$setting->name] = $setting->value;
|
||||
}
|
||||
if (empty($full)) {
|
||||
return $settings;
|
||||
} else {
|
||||
$settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settings);
|
||||
$settingsFlattened = $this->SettingsProvider->flattenSettingsConfiguration($settingsProvider);
|
||||
$notices = $this->SettingsProvider->getNoticesFromSettingsConfiguration($settingsProvider, $settings);
|
||||
return [
|
||||
'settings' => $settings,
|
||||
'settingsProvider' => $settingsProvider,
|
||||
'settingsFlattened' => $settingsFlattened,
|
||||
'notices' => $notices,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function getSettingsForUser($user_id)
|
||||
{
|
||||
return $this->find()->where([
|
||||
'user_id' => $user_id,
|
||||
])->all();
|
||||
}
|
||||
|
||||
public function getSettingByName($user, $name)
|
||||
{
|
||||
return $this->find()->where([
|
||||
'user_id' => $user->id,
|
||||
'name' => $name,
|
||||
])->first();
|
||||
}
|
||||
|
||||
public function createSetting($user, $name, $value)
|
||||
{
|
||||
$setting = $this->newEmptyEntity();
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'value' => $value,
|
||||
'user_id' => $user->id,
|
||||
];
|
||||
$setting = $this->patchEntity($setting, $data);
|
||||
$savedData = $this->save($setting);
|
||||
return $savedData;
|
||||
}
|
||||
|
||||
public function editSetting($user, $name, $value)
|
||||
{
|
||||
$setting = $this->getSettingByName($user, $name);
|
||||
$setting = $this->patchEntity($setting, [
|
||||
'value' => $value
|
||||
]);
|
||||
$savedData = $this->save($setting);
|
||||
return $savedData;
|
||||
}
|
||||
|
||||
public function saveBookmark($user, $data)
|
||||
{
|
||||
$setting = $this->getSettingByName($user, $this->BOOKMARK_SETTING_NAME);
|
||||
$bookmarkData = [
|
||||
'label' => $data['bookmark_label'],
|
||||
'name' => $data['bookmark_name'],
|
||||
'url' => $data['bookmark_url'],
|
||||
];
|
||||
if (is_null($setting)) { // setting not found, create it
|
||||
$bookmarksData = json_encode([$bookmarkData]);
|
||||
$result = $this->createSetting($user, $this->BOOKMARK_SETTING_NAME, $bookmarksData);
|
||||
} else {
|
||||
$bookmarksData = json_decode($setting->value);
|
||||
$bookmarksData[] = $bookmarkData;
|
||||
$bookmarksData = json_encode($bookmarksData);
|
||||
$result = $this->editSetting($user, $this->BOOKMARK_SETTING_NAME, $bookmarksData);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function deleteBookmark($user, $data)
|
||||
{
|
||||
$setting = $this->getSettingByName($user, $this->BOOKMARK_SETTING_NAME);
|
||||
$bookmarkData = [
|
||||
'name' => $data['bookmark_name'],
|
||||
'url' => $data['bookmark_url'],
|
||||
];
|
||||
if (is_null($setting)) { // Can't delete something that doesn't exist
|
||||
return null;
|
||||
} else {
|
||||
$bookmarksData = json_decode($setting->value, true);
|
||||
foreach ($bookmarksData as $i => $savedBookmark) {
|
||||
if ($savedBookmark['name'] == $bookmarkData['name'] && $savedBookmark['url'] == $bookmarkData['url']) {
|
||||
unset($bookmarksData[$i]);
|
||||
}
|
||||
}
|
||||
$bookmarksData = json_encode($bookmarksData);
|
||||
$result = $this->editSetting($user, $this->BOOKMARK_SETTING_NAME, $bookmarksData);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
|
@ -34,6 +34,13 @@ class UsersTable extends AppTable
|
|||
'cascadeCallbacks' => false
|
||||
]
|
||||
);
|
||||
$this->hasMany(
|
||||
'UserSettings',
|
||||
[
|
||||
'dependent' => true,
|
||||
'cascadeCallbacks' => true
|
||||
]
|
||||
);
|
||||
$this->setDisplayField('username');
|
||||
}
|
||||
|
||||
|
|
|
@ -139,6 +139,12 @@ class BootstrapHelper extends Helper
|
|||
$bsSwitch = new BoostrapSwitch($options, $this);
|
||||
return $bsSwitch->switch();
|
||||
}
|
||||
|
||||
public function dropdownMenu($options)
|
||||
{
|
||||
$bsDropdownMenu = new BoostrapDropdownMenu($options, $this);
|
||||
return $bsDropdownMenu->dropdownMenu();
|
||||
}
|
||||
}
|
||||
|
||||
class BootstrapGeneric
|
||||
|
@ -652,21 +658,21 @@ class BoostrapTable extends BootstrapGeneric {
|
|||
$key = $field;
|
||||
}
|
||||
$cellValue = Hash::get($row, $key);
|
||||
$html .= $this->genCell($cellValue, $field, $row);
|
||||
$html .= $this->genCell($cellValue, $field, $row, $i);
|
||||
}
|
||||
} else { // indexed array
|
||||
foreach ($row as $cellValue) {
|
||||
$html .= $this->genCell($cellValue, $field, $row);
|
||||
$html .= $this->genCell($cellValue, $field, $row, $i);
|
||||
}
|
||||
}
|
||||
$html .= $this->closeNode('tr');
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genCell($value, $field=[], $row=[])
|
||||
private function genCell($value, $field=[], $row=[], $i=0)
|
||||
{
|
||||
if (isset($field['formatter'])) {
|
||||
$cellContent = $field['formatter']($value, $row);
|
||||
$cellContent = $field['formatter']($value, $row, $i);
|
||||
} else if (isset($field['element'])) {
|
||||
$cellContent = $this->btHelper->getView()->element($field['element'], [
|
||||
'data' => [$value],
|
||||
|
@ -909,7 +915,7 @@ class BoostrapButton extends BootstrapGeneric {
|
|||
{
|
||||
if (!empty($this->options['icon'])) {
|
||||
$bsIcon = new BoostrapIcon($this->options['icon'], [
|
||||
'class' => [(!empty($this->options['title']) ? 'me-1' : '')]
|
||||
'class' => [(!empty($this->options['text']) ? 'me-1' : '')]
|
||||
]);
|
||||
return $bsIcon->icon();
|
||||
}
|
||||
|
@ -1664,3 +1670,137 @@ class BootstrapListGroup extends BootstrapGeneric
|
|||
return !empty($item['body']) ? h($item['body']) : '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class BoostrapDropdownMenu extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'dropdown-class' => [],
|
||||
'toggle-button' => [],
|
||||
'menu' => [],
|
||||
'direction' => 'end',
|
||||
'alignment' => 'start',
|
||||
'submenu_alignment' => 'start',
|
||||
'submenu_direction' => 'end',
|
||||
'submenu_classes' => [],
|
||||
];
|
||||
|
||||
function __construct($options, $btHelper) {
|
||||
$this->allowedOptionValues = [
|
||||
'direction' => ['start', 'end', 'up', 'down'],
|
||||
'alignment' => ['start', 'end'],
|
||||
];
|
||||
$this->processOptions($options);
|
||||
$this->menu = $this->options['menu'];
|
||||
$this->btHelper = $btHelper;
|
||||
}
|
||||
|
||||
private function processOptions($options)
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
if (!empty($this->options['dropdown-class']) && !is_array($this->options['dropdown-class'])) {
|
||||
$this->options['dropdown-class'] = [$this->options['dropdown-class']];
|
||||
}
|
||||
$this->checkOptionValidity();
|
||||
}
|
||||
|
||||
public function dropdownMenu()
|
||||
{
|
||||
return $this->fullDropdown();
|
||||
}
|
||||
|
||||
public function fullDropdown()
|
||||
{
|
||||
return $this->genDropdownWrapper($this->genDropdownToggleButton(), $this->genDropdownMenu($this->menu));
|
||||
}
|
||||
|
||||
public function genDropdownWrapper($toggle='', $menu='', $direction=null, $classes=null)
|
||||
{
|
||||
$classes = !is_null($classes) ? $classes : $this->options['dropdown-class'];
|
||||
$direction = !is_null($direction) ? $direction : $this->options['direction'];
|
||||
$content = $toggle . $menu;
|
||||
$html = $this->genNode('div', array_merge(
|
||||
$this->options['params'],
|
||||
[
|
||||
'class' => array_merge(
|
||||
$classes,
|
||||
[
|
||||
'dropdown',
|
||||
"drop{$direction}"
|
||||
]
|
||||
)
|
||||
]
|
||||
), $content);
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function genDropdownToggleButton()
|
||||
{
|
||||
$defaultOptions = [
|
||||
'class' => ['dropdown-toggle'],
|
||||
'params' => [
|
||||
'data-bs-toggle' => 'dropdown',
|
||||
'aria-expanded' => 'false',
|
||||
]
|
||||
];
|
||||
$options = array_merge($this->options['toggle-button'], $defaultOptions);
|
||||
return $this->btHelper->button($options);
|
||||
}
|
||||
|
||||
private function genDropdownMenu($entries, $alignment=null)
|
||||
{
|
||||
$alignment = !is_null($alignment) ? $alignment : $this->options['alignment'];
|
||||
$html = $this->genNode('div', [
|
||||
'class' => ['dropdown-menu', "dropdown-menu-{$alignment}"],
|
||||
], $this->genEntries($entries));
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genEntries($entries)
|
||||
{
|
||||
$html = '';
|
||||
foreach ($entries as $entry) {
|
||||
$link = $this->genEntry($entry);
|
||||
if (!empty($entry['menu'])) {
|
||||
$html .= $this->genDropdownWrapper($link, $this->genDropdownMenu($entry['menu']), $this->options['submenu_direction'], $this->options['submenu_classes']);
|
||||
} else {
|
||||
$html .= $link;
|
||||
}
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genEntry($entry)
|
||||
{
|
||||
if (!empty($entry['html'])) {
|
||||
return $entry['html'];
|
||||
}
|
||||
|
||||
$classes = ['dropdown-item'];
|
||||
$params = ['href' => '#'];
|
||||
$icon = '';
|
||||
if (!empty($entry['icon'])) {
|
||||
$icon = $this->btHelper->icon($entry['icon']);
|
||||
}
|
||||
|
||||
if (!empty($entry['menu'])) {
|
||||
$classes[] = 'dropdown-toggle';
|
||||
$params['data-bs-toggle'] = 'dropdown';
|
||||
$params['aria-haspopup'] = 'true';
|
||||
$params['aria-expanded'] = 'false';
|
||||
if (!empty($entry['keepOpen'])) {
|
||||
$classes[] = 'open-form';
|
||||
}
|
||||
$params['data-open-form-id'] = mt_rand();
|
||||
}
|
||||
|
||||
$label = $this->genNode('span', [
|
||||
'class' => ['ms-2',],
|
||||
], h($entry['text']));
|
||||
$content = $icon . $label;
|
||||
|
||||
return $this->genNode('a', array_merge([
|
||||
'class' => $classes,
|
||||
], $params), $content);
|
||||
}
|
||||
}
|
|
@ -25,6 +25,10 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
'allowFilering' => true
|
||||
],
|
||||
[
|
||||
'type' => 'table_action',
|
||||
'table_setting_id' => 'individual_index',
|
||||
]
|
||||
]
|
||||
],
|
||||
|
|
|
@ -1,15 +1,42 @@
|
|||
<?php
|
||||
// $Parsedown = new Parsedown();
|
||||
// echo $Parsedown->text($md);
|
||||
$bookmarks = !empty($loggedUser->user_settings_by_name['ui.bookmarks']['value']) ? json_decode($loggedUser->user_settings_by_name['ui.bookmarks']['value'], true) : [];
|
||||
?>
|
||||
|
||||
<h2><?= __('Home') ?></h2>
|
||||
<h3>
|
||||
<?= $this->Bootstrap->icon('bookmark', [
|
||||
'class' => ['fa-fw']
|
||||
]); ?>
|
||||
<?= __('Bookmarks') ?>
|
||||
</h3>
|
||||
<div class="row">
|
||||
<?php foreach ($statistics as $modelName => $statistics): ?>
|
||||
<? if (!empty($bookmarks)) : ?>
|
||||
<ul class="col-sm-12 col-md-10 col-l-8 col-xl-8 mb-3">
|
||||
<?php foreach ($bookmarks as $bookmark) : ?>
|
||||
<li class="list-group-item">
|
||||
<a href="<?= h($bookmark['url']) ?>" class="w-bold">
|
||||
<?= h($bookmark['label']) ?>
|
||||
</a>
|
||||
<span class="ms-3 fw-light"><?= h($bookmark['name']) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php else : ?>
|
||||
<p class="fw-light"><?= __('No bookmarks') ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<?= $this->Bootstrap->icon('chart-bar', [
|
||||
'class' => ['fa-fw']
|
||||
]); ?>
|
||||
<?= __('Activity') ?>
|
||||
</h3>
|
||||
<div class="row">
|
||||
<?php foreach ($statistics as $modelName => $statistics) : ?>
|
||||
<div class="col-sm-6 col-md-5 col-l-4 col-xl-3 mb-3">
|
||||
<?php
|
||||
$exploded = explode('.', $modelName);
|
||||
$modelForDisplay = $exploded[count($exploded)-1];
|
||||
$modelForDisplay = $exploded[count($exploded) - 1];
|
||||
$panelTitle = $this->Html->link(
|
||||
h($modelForDisplay),
|
||||
$this->Url->build([
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
echo $this->element('genericElements/Form/genericForm', [
|
||||
'data' => [
|
||||
'description' => __('Authkeys are used for API access. A user can have more than one authkey, so if you would like to use separate keys per tool that queries Cerebrate, add additional keys. Use the comment field to make identifying your keys easier.'),
|
||||
'description' => __('Application setting form'),
|
||||
'fields' => [
|
||||
[
|
||||
'field' => 'name',
|
||||
|
|
|
@ -5,294 +5,69 @@ $variantFromSeverity = [
|
|||
'warning' => 'warning',
|
||||
'info' => 'info',
|
||||
];
|
||||
$this->set('variantFromSeverity', $variantFromSeverity);
|
||||
$settingTable = genNavcard($settingsProvider, $this);
|
||||
|
||||
$navLinks = [];
|
||||
$tabContents = [];
|
||||
|
||||
foreach ($settingsProvider as $settingTitle => $settingContent) {
|
||||
$navLinks[] = h($settingTitle);
|
||||
$tabContents[] = $this->element('Settings/category', [
|
||||
'settings' => $settingContent,
|
||||
'includeScrollspy' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
array_unshift($navLinks, __('Settings Diagnostic'));
|
||||
$notice = $this->element('Settings/notice', [
|
||||
'variantFromSeverity' => $variantFromSeverity,
|
||||
'notices' => $notices,
|
||||
]);
|
||||
array_unshift($tabContents, $notice);
|
||||
?>
|
||||
|
||||
<script>
|
||||
const variantFromSeverity = <?= json_encode($variantFromSeverity) ?>;
|
||||
const settingsFlattened = <?= json_encode($settingsFlattened) ?>;
|
||||
window.variantFromSeverity = <?= json_encode($variantFromSeverity) ?>;
|
||||
window.settingsFlattened = <?= json_encode($settingsFlattened) ?>;
|
||||
window.saveSettingURL = '/instance/saveSetting'
|
||||
</script>
|
||||
|
||||
<div class="px-5">
|
||||
<div class="mb-3 mt-2">
|
||||
<?=
|
||||
$this->element('Settings/search', [
|
||||
'settingsFlattened' => $settingsFlattened,
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
<?= $settingTable; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
function genNavcard($settingsProvider, $appView)
|
||||
{
|
||||
$cardContent = [];
|
||||
$cardNavs = array_keys($settingsProvider);
|
||||
foreach ($settingsProvider as $navName => $sectionSettings) {
|
||||
if (!empty($sectionSettings)) {
|
||||
$cardContent[] = genContentForNav($sectionSettings, $appView);
|
||||
} else {
|
||||
$cardContent[] = __('No Settings available yet');
|
||||
}
|
||||
}
|
||||
array_unshift($cardNavs, __('Settings Diagnostic'));
|
||||
$notice = $appView->element('Settings/notice', [
|
||||
'variantFromSeverity' => $appView->get('variantFromSeverity'),
|
||||
]);
|
||||
array_unshift($cardContent, $notice);
|
||||
$tabsOptions0 = [
|
||||
// 'vertical' => true,
|
||||
// 'vertical-size' => 2,
|
||||
<?php
|
||||
$tabsOptions = [
|
||||
'card' => false,
|
||||
'pills' => false,
|
||||
'justify' => 'center',
|
||||
'nav-class' => ['settings-tabs'],
|
||||
'data' => [
|
||||
'navs' => $cardNavs,
|
||||
'content' => $cardContent
|
||||
'navs' => $navLinks,
|
||||
'content' => $tabContents
|
||||
]
|
||||
];
|
||||
$table0 = $appView->Bootstrap->tabs($tabsOptions0);
|
||||
return $table0;
|
||||
}
|
||||
|
||||
function genContentForNav($sectionSettings, $appView)
|
||||
{
|
||||
$groupedContent = [];
|
||||
$groupedSetting = [];
|
||||
foreach ($sectionSettings as $sectionName => $subSectionSettings) {
|
||||
if (!empty($subSectionSettings)) {
|
||||
$groupedContent[] = genSection($sectionName, $subSectionSettings, $appView);
|
||||
} else {
|
||||
$groupedContent[] = '';
|
||||
}
|
||||
if (!isLeaf($subSectionSettings)) {
|
||||
$groupedSetting[$sectionName] = array_filter( // only show grouped settings
|
||||
array_keys($subSectionSettings),
|
||||
function ($settingGroupName) use ($subSectionSettings) {
|
||||
return !isLeaf($subSectionSettings[$settingGroupName]) && !empty($subSectionSettings[$settingGroupName]);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
$contentHtml = implode('', $groupedContent);
|
||||
$scrollspyNav = $appView->element('Settings/scrollspyNav', [
|
||||
'groupedSetting' => $groupedSetting
|
||||
]);
|
||||
$mainPanelHeight = 'calc(100vh - 42px - 1rem - 56px - 38px - 1rem)';
|
||||
$container = '<div class="d-flex">';
|
||||
$container .= "<div class=\"\" style=\"flex: 0 0 10em;\">{$scrollspyNav}</div>";
|
||||
$container .= "<div data-bs-spy=\"scroll\" data-bs-target=\"#navbar-scrollspy-setting\" data-bs-offset=\"25\" style=\"height: {$mainPanelHeight}\" class=\"p-3 overflow-auto position-relative flex-grow-1\">{$contentHtml}</div>";
|
||||
$container .= '</div>';
|
||||
return $container;
|
||||
}
|
||||
|
||||
function genSection($sectionName, $subSectionSettings, $appView)
|
||||
{
|
||||
$sectionContent = [];
|
||||
$sectionContent[] = '<div>';
|
||||
if (isLeaf($subSectionSettings)) {
|
||||
$panelHTML = $appView->element('Settings/panel', [
|
||||
'sectionName' => $sectionName,
|
||||
'panelName' => $sectionName,
|
||||
'panelSettings' => $subSectionSettings,
|
||||
]);
|
||||
$sectionContent[] = $panelHTML;
|
||||
} else {
|
||||
if (count($subSectionSettings) > 0) {
|
||||
$sectionContent[] = sprintf('<h2 id="%s">%s</h2>', getResolvableID($sectionName), h($sectionName));
|
||||
}
|
||||
foreach ($subSectionSettings as $panelName => $panelSettings) {
|
||||
if (!empty($panelSettings)) {
|
||||
$panelHTML = $appView->element('Settings/panel', [
|
||||
'sectionName' => $sectionName,
|
||||
'panelName' => $panelName,
|
||||
'panelSettings' => $panelSettings,
|
||||
]);
|
||||
$sectionContent[] = $panelHTML;
|
||||
} else {
|
||||
$sectionContent[] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
$sectionContent[] = '</div>';
|
||||
return implode('', $sectionContent);
|
||||
}
|
||||
|
||||
function isLeaf($setting)
|
||||
{
|
||||
return !empty($setting['name']) && !empty($setting['type']);
|
||||
}
|
||||
|
||||
function getResolvableID($sectionName, $panelName=false)
|
||||
{
|
||||
$id = sprintf('sp-%s', preg_replace('/(\.|\W)/', '_', h($sectionName)));
|
||||
if (!empty($panelName)) {
|
||||
$id .= '-' . preg_replace('/(\.|\W)/', '_', h($panelName));
|
||||
}
|
||||
return $id;
|
||||
}
|
||||
?>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
new bootstrap.Tooltip('.depends-on-icon', {
|
||||
placement: 'right',
|
||||
})
|
||||
$('select.custom-select[multiple]').select2()
|
||||
|
||||
$('.settings-tabs a[data-bs-toggle="tab"]').on('shown.bs.tab', function (event) {
|
||||
$('[data-bs-spy="scroll"]').trigger('scroll.bs.scrollspy')
|
||||
})
|
||||
|
||||
$('.tab-content input, .tab-content select').on('input', function() {
|
||||
if ($(this).attr('type') == 'checkbox') {
|
||||
const $input = $(this)
|
||||
const $inputGroup = $(this).closest('.setting-group')
|
||||
const settingName = $(this).data('setting-name')
|
||||
const settingValue = $(this).is(':checked') ? 1 : 0
|
||||
saveSetting($inputGroup[0], $input, settingName, settingValue)
|
||||
} else {
|
||||
handleSettingValueChange($(this))
|
||||
}
|
||||
})
|
||||
|
||||
$('.tab-content .setting-group .btn-save-setting').click(function() {
|
||||
const $input = $(this).closest('.input-group').find('input, select')
|
||||
const settingName = $input.data('setting-name')
|
||||
const settingValue = $input.val()
|
||||
saveSetting(this, $input, settingName, settingValue)
|
||||
})
|
||||
$('.tab-content .setting-group .btn-reset-setting').click(function() {
|
||||
const $btn = $(this)
|
||||
const $input = $btn.closest('.input-group').find('input, select')
|
||||
let oldValue = settingsFlattened[$input.data('setting-name')].value
|
||||
if ($input.is('select')) {
|
||||
oldValue = oldValue !== undefined ? oldValue : -1
|
||||
} else {
|
||||
oldValue = oldValue !== undefined ? oldValue : ''
|
||||
}
|
||||
$input.val(oldValue)
|
||||
handleSettingValueChange($input)
|
||||
})
|
||||
|
||||
const referencedID = window.location.hash
|
||||
redirectToSetting(referencedID)
|
||||
})
|
||||
|
||||
function saveSetting(statusNode, $input, settingName, settingValue) {
|
||||
const url = '/instance/saveSetting/'
|
||||
const data = {
|
||||
name: settingName,
|
||||
value: settingValue,
|
||||
}
|
||||
const APIOptions = {
|
||||
statusNode: statusNode,
|
||||
}
|
||||
AJAXApi.quickFetchAndPostForm(url, data, APIOptions).then((result) => {
|
||||
settingsFlattened[settingName] = result.data
|
||||
if ($input.attr('type') == 'checkbox') {
|
||||
$input.prop('checked', result.data.value)
|
||||
} else {
|
||||
$input.val(result.data.value)
|
||||
}
|
||||
handleSettingValueChange($input)
|
||||
}).catch((e) => {})
|
||||
}
|
||||
|
||||
function handleSettingValueChange($input) {
|
||||
const oldValue = settingsFlattened[$input.data('setting-name')].value
|
||||
const newValue = ($input.attr('type') == 'checkbox' ? $input.is(':checked') : $input.val())
|
||||
if (newValue == oldValue || (newValue == '' && oldValue == undefined)) {
|
||||
restoreWarnings($input)
|
||||
} else {
|
||||
removeWarnings($input)
|
||||
}
|
||||
}
|
||||
|
||||
function removeWarnings($input) {
|
||||
const $inputGroup = $input.closest('.input-group')
|
||||
const $btnSettingAction = $inputGroup.find('.btn-setting-action')
|
||||
const $saveButton = $('.setting-group button.btn-save-setting')
|
||||
$input.removeClass(['is-invalid', 'border-warning', 'border-danger', 'border-info', 'warning', 'info'])
|
||||
$btnSettingAction.removeClass('d-none')
|
||||
if ($input.is('select') && $input.find('option:selected').data('is-empty-option') == 1) {
|
||||
$btnSettingAction.addClass('d-none') // hide save button if empty selection picked
|
||||
}
|
||||
$inputGroup.parent().find('.invalid-feedback').removeClass('d-block')
|
||||
}
|
||||
|
||||
function restoreWarnings($input) {
|
||||
const $inputGroup = $input.closest('.input-group')
|
||||
const $btnSettingAction = $inputGroup.find('.btn-setting-action')
|
||||
const $saveButton = $('.setting-group button.btn-save-setting')
|
||||
const setting = settingsFlattened[$input.data('setting-name')]
|
||||
if (setting.error) {
|
||||
borderVariant = setting.severity !== undefined ? variantFromSeverity[setting.severity] : 'warning'
|
||||
$input.addClass(['is-invalid', `border-${borderVariant}`, borderVariant])
|
||||
$inputGroup.parent().find('.invalid-feedback').addClass('d-block').text(setting.errorMessage)
|
||||
} else {
|
||||
removeWarnings($input)
|
||||
}
|
||||
const $callout = $input.closest('.settings-group')
|
||||
updateCalloutColors($callout)
|
||||
$btnSettingAction.addClass('d-none')
|
||||
}
|
||||
|
||||
function updateCalloutColors($callout) {
|
||||
if ($callout.length == 0) {
|
||||
return
|
||||
}
|
||||
const $settings = $callout.find('input, select')
|
||||
const settingNames = Array.from($settings).map((i) => {
|
||||
return $(i).data('setting-name')
|
||||
})
|
||||
const severityMapping = {null: 0, info: 1, warning: 2, critical: 3}
|
||||
const severityMappingInverted = Object.assign({}, ...Object.entries(severityMapping).map(([k, v]) => ({[v]: k})))
|
||||
let highestSeverity = severityMapping[null]
|
||||
settingNames.forEach(name => {
|
||||
if (settingsFlattened[name].error) {
|
||||
highestSeverity = severityMapping[settingsFlattened[name].severity] > highestSeverity ? severityMapping[settingsFlattened[name].severity] : highestSeverity
|
||||
}
|
||||
});
|
||||
highestSeverity = severityMappingInverted[highestSeverity]
|
||||
$callout.removeClass(['callout', 'callout-danger', 'callout-warning', 'callout-info'])
|
||||
if (highestSeverity !== null) {
|
||||
$callout.addClass(['callout', `callout-${variantFromSeverity[highestSeverity]}`])
|
||||
}
|
||||
}
|
||||
|
||||
function redirectToSetting(referencedID) {
|
||||
const $settingToFocus = $(referencedID)
|
||||
const pageNavID = $(referencedID).closest('.tab-pane').attr('aria-labelledby')
|
||||
const $navController = $(`#${pageNavID}`)
|
||||
const $settingGroup = $settingToFocus.closest('.settings-group')
|
||||
$navController
|
||||
.on('shown.bs.tab.after-redirect', () => {
|
||||
$settingToFocus[0].scrollIntoView()
|
||||
const inputID = $settingToFocus.parent().attr('for')
|
||||
$settingToFocus.closest('.setting-group').find(`#${inputID}`).focus()
|
||||
$navController.off('shown.bs.tab.after-redirect')
|
||||
$settingGroup.addClass(['to-be-slided', 'slide-in'])
|
||||
})
|
||||
.tab('show')
|
||||
$settingGroup.on('webkitAnimationEnd oanimationend msAnimationEnd animationend', function() {
|
||||
$(this).removeClass(['to-be-slided', 'slide-in'])
|
||||
});
|
||||
}
|
||||
</script>
|
||||
echo $this->Bootstrap->tabs($tabsOptions);
|
||||
echo $this->Html->script('settings');
|
||||
?>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.input-group-actions {
|
||||
z-index: 5;
|
||||
}
|
||||
.form-control[type="number"] ~ div > a.btn-reset-setting {
|
||||
|
||||
.form-control[type="number"]~div>a.btn-reset-setting {
|
||||
left: -3em;
|
||||
}
|
||||
select.custom-select[multiple][data-setting-name] ~ span.select2-container{
|
||||
|
||||
select.custom-select[multiple][data-setting-name]~span.select2-container {
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
span.select2-container--open {
|
||||
min-width: unset;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,10 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
'allowFilering' => true
|
||||
],
|
||||
[
|
||||
'type' => 'table_action',
|
||||
'table_setting_id' => 'organisation_index',
|
||||
]
|
||||
]
|
||||
],
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
echo $this->element('genericElements/Form/genericForm', [
|
||||
'data' => [
|
||||
'description' => __('User settings are used to register setting tied to user profile.'),
|
||||
'model' => 'UserSettings',
|
||||
'fields' => [
|
||||
[
|
||||
'field' => 'user_id',
|
||||
'type' => 'dropdown',
|
||||
'label' => __('User'),
|
||||
'options' => $dropdownData['user'],
|
||||
'value' => !empty($user_id) ? $user_id : '',
|
||||
'disabled' => !empty($user_id),
|
||||
],
|
||||
[
|
||||
'field' => 'name',
|
||||
'label' => __('Setting Name'),
|
||||
],
|
||||
[
|
||||
'field' => 'value',
|
||||
'label' => __('Setting Value'),
|
||||
'type' => 'codemirror',
|
||||
'codemirror' => [
|
||||
'height' => '10rem',
|
||||
'mode' => [
|
||||
'name' => 'text',
|
||||
],
|
||||
]
|
||||
],
|
||||
],
|
||||
'submit' => [
|
||||
'action' => $this->request->getParam('action')
|
||||
]
|
||||
]
|
||||
]);
|
||||
?>
|
||||
</div>
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
echo $this->element('genericElements/Form/genericForm', [
|
||||
'data' => [
|
||||
'title' => __('Delete a bookmark'),
|
||||
'description' => __('Specify the name and the URL of the bookmark to be deleted from your user profile.'),
|
||||
'model' => 'UserSettings',
|
||||
'fields' => [
|
||||
[
|
||||
'field' => 'bookmark_name',
|
||||
'label' => __('Name'),
|
||||
'placeholder' => __('Home page'),
|
||||
],
|
||||
[
|
||||
'field' => 'bookmark_url',
|
||||
'label' => __('URL'),
|
||||
'placeholder' => '/instance/home',
|
||||
],
|
||||
],
|
||||
'submit' => [
|
||||
'action' => $this->request->getParam('action')
|
||||
]
|
||||
],
|
||||
]);
|
||||
?>
|
||||
</div>
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
$title = __('User Setting index');
|
||||
if (!empty($settingsForUser)) {
|
||||
$title .= __(' of {0}', $settingsForUser->username);
|
||||
}
|
||||
|
||||
echo $this->element('genericElements/IndexTable/index_table', [
|
||||
'data' => [
|
||||
'data' => $data,
|
||||
'top_bar' => [
|
||||
'children' => [
|
||||
[
|
||||
'type' => 'simple',
|
||||
'children' => [
|
||||
'data' => [
|
||||
'type' => 'simple',
|
||||
'text' => __('Add Setting'),
|
||||
'class' => 'btn btn-primary',
|
||||
'popover_url' => sprintf('/userSettings/add/%s', h($this->request->getQuery('Users_id')))
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
]
|
||||
]
|
||||
],
|
||||
'fields' => [
|
||||
[
|
||||
'name' => '#',
|
||||
'sort' => 'id',
|
||||
'data_path' => 'id',
|
||||
],
|
||||
[
|
||||
'name' => __('User'),
|
||||
'sort' => 'user.username',
|
||||
'data_path' => 'user.username',
|
||||
'url' => '/users/view/{{0}}',
|
||||
'url_vars' => ['user.id']
|
||||
],
|
||||
[
|
||||
'name' => __('Setting Name'),
|
||||
'sort' => 'name',
|
||||
'data_path' => 'name',
|
||||
],
|
||||
[
|
||||
'name' => __('Setting Value'),
|
||||
'sort' => 'value',
|
||||
'data_path' => 'value',
|
||||
'class' => 'font-monospace'
|
||||
],
|
||||
],
|
||||
'title' => $title,
|
||||
'description' => __('The list of user settings in this Cerebrate instance. All users can have setting tied to their user profile.'),
|
||||
'actions' => [
|
||||
[
|
||||
'open_modal' => '/userSettings/edit/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'edit'
|
||||
],
|
||||
[
|
||||
'open_modal' => '/userSettings/delete/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'trash'
|
||||
],
|
||||
]
|
||||
]
|
||||
]);
|
||||
echo '</div>';
|
||||
?>
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
echo $this->element('genericElements/Form/genericForm', [
|
||||
'data' => [
|
||||
'title' => __('Add a bookmark'),
|
||||
'description' => __('Specify the name and the URL of the bookmark which will be tied to your user profile.'),
|
||||
'model' => 'UserSettings',
|
||||
'fields' => [
|
||||
[
|
||||
'field' => 'bookmark_name',
|
||||
'label' => __('Name'),
|
||||
'placeholder' => __('Home page'),
|
||||
],
|
||||
[
|
||||
'field' => 'bookmark_label',
|
||||
'label' => __('Label'),
|
||||
'placeholder' => __('Home'),
|
||||
],
|
||||
[
|
||||
'field' => 'bookmark_url',
|
||||
'label' => __('URL'),
|
||||
'placeholder' => '/instance/home',
|
||||
],
|
||||
],
|
||||
'submit' => [
|
||||
'action' => $this->request->getParam('action')
|
||||
]
|
||||
],
|
||||
]);
|
||||
?>
|
||||
</div>
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
echo $this->element('genericElements/Form/genericForm', [
|
||||
'data' => [
|
||||
'description' => __('User settings form'),
|
||||
'fields' => [
|
||||
[
|
||||
'field' => 'name',
|
||||
],
|
||||
[
|
||||
'field' => 'value'
|
||||
],
|
||||
],
|
||||
'submit' => [
|
||||
'action' => $this->request->getParam('action')
|
||||
]
|
||||
]
|
||||
]);
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
echo $this->element('genericElements/Form/genericForm', [
|
||||
'data' => [
|
||||
'description' => __('User settings are used to register setting tied to user profile.'),
|
||||
'model' => 'UserSettings',
|
||||
'fields' => [
|
||||
sprintf(
|
||||
'<div class="row mb-3"><div class="col-sm-2 form-label">%s</div><div class="col-sm-10 font-monospace">%s</div>',
|
||||
__('Setting Name'),
|
||||
h($settingName)
|
||||
),
|
||||
[
|
||||
'field' => 'value',
|
||||
'label' => __('Setting Value'),
|
||||
'type' => 'codemirror',
|
||||
'codemirror' => [
|
||||
'height' => '10rem',
|
||||
'mode' => [
|
||||
'name' => 'text',
|
||||
],
|
||||
]
|
||||
],
|
||||
],
|
||||
'submit' => [
|
||||
'action' => $this->request->getParam('action')
|
||||
]
|
||||
]
|
||||
]);
|
||||
?>
|
||||
</div>
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
echo $this->element(
|
||||
'/genericElements/SingleViews/single_view',
|
||||
[
|
||||
'data' => $entity,
|
||||
'fields' => [
|
||||
[
|
||||
'key' => __('ID'),
|
||||
'path' => 'id'
|
||||
],
|
||||
[
|
||||
'key' => __('Name'),
|
||||
'path' => 'name'
|
||||
],
|
||||
[
|
||||
'key' => __('Value'),
|
||||
'path' => 'value'
|
||||
],
|
||||
[
|
||||
'key' => __('Created'),
|
||||
'path' => 'created'
|
||||
],
|
||||
[
|
||||
'key' => __('Modified'),
|
||||
'path' => 'modified'
|
||||
],
|
||||
[
|
||||
'key' => __('User'),
|
||||
'path' => 'user.username',
|
||||
'url' => '/users/view/{{0}}',
|
||||
'url_vars' => 'user.id'
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
|
@ -75,6 +75,13 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'url' => '/roles/view/{{0}}',
|
||||
'url_vars' => ['role.id']
|
||||
],
|
||||
[
|
||||
'name' => __('# User Settings'),
|
||||
'element' => 'count_summary',
|
||||
'data_path' => 'user_settings',
|
||||
'url' => '/user-settings/index?Users.id={{url_data}}',
|
||||
'url_data_path' => 'id'
|
||||
],
|
||||
],
|
||||
'title' => __('User index'),
|
||||
'description' => __('The list of enrolled users in this Cerebrate instance. All of the users have or at one point had access to the system.'),
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
$navLinks = [];
|
||||
$tabContents = [];
|
||||
|
||||
foreach ($settingsProvider as $settingTitle => $settingContent) {
|
||||
$navLinks[] = h($settingTitle);
|
||||
$tabContents[] = $this->element('Settings/category', [
|
||||
'settings' => $settingContent,
|
||||
'includeScrollspy' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
$navLinks[] = __('Bookmarks');
|
||||
$tabContents[] = $this->element('UserSettings/saved-bookmarks', [
|
||||
'bookmarks' => !empty($user->user_settings_by_name['ui.bookmarks']['value']) ? json_decode($user->user_settings_by_name['ui.bookmarks']['value'], true) : []
|
||||
]);
|
||||
|
||||
$tabsOptions = [
|
||||
'vertical' => true,
|
||||
'vertical-size' => 2,
|
||||
'card' => true,
|
||||
'pills' => true,
|
||||
'justify' => 'center',
|
||||
'nav-class' => ['settings-tabs'],
|
||||
'data' => [
|
||||
'navs' => $navLinks,
|
||||
'content' => $tabContents
|
||||
]
|
||||
];
|
||||
$tabs = $this->Bootstrap->tabs($tabsOptions);
|
||||
echo $this->Html->script('settings');
|
||||
?>
|
||||
|
||||
<script>
|
||||
window.settingsFlattened = <?= json_encode($settingsFlattened) ?>;
|
||||
window.saveSettingURL = '/userSettings/saveSetting'
|
||||
</script>
|
||||
|
||||
<h2 class="fw-light"><?= __('Account settings') ?></h2>
|
||||
<div class="p-2">
|
||||
<div>
|
||||
<div>
|
||||
<span class="fw-bold font-monospace me-2 fs-5"><?= h($user->username) ?></span>
|
||||
<span><?= h($user->individual->full_name) ?></span>
|
||||
</div>
|
||||
<div class="fw-light"><?= __('Your personnal account') ?></div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<?= $tabs ?>
|
||||
</div>
|
||||
</div>
|
|
@ -51,6 +51,11 @@ echo $this->element(
|
|||
'url' => '/EncryptionKeys/index?Users.id={{0}}',
|
||||
'url_params' => ['id'],
|
||||
'title' => __('Encryption keys')
|
||||
],
|
||||
[
|
||||
'url' => '/UserSettings/index?Users.id={{0}}',
|
||||
'url_params' => ['id'],
|
||||
'title' => __('User settings')
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
if (!function_exists('isLeaf')) {
|
||||
function isLeaf($setting)
|
||||
{
|
||||
return !empty($setting['name']) && !empty($setting['type']);
|
||||
}
|
||||
}
|
||||
|
||||
$variantFromSeverity = [
|
||||
'critical' => 'danger',
|
||||
'warning' => 'warning',
|
||||
'info' => 'info',
|
||||
];
|
||||
$this->set('variantFromSeverity', $variantFromSeverity);
|
||||
$includeScrollspy = !empty($includeScrollspy);
|
||||
|
||||
$groupedContent = [];
|
||||
$scrollSpyContent = [];
|
||||
foreach ($settings as $sectionName => $sectionContent) {
|
||||
if (!empty($sectionContent)) {
|
||||
$groupedContent[] = $this->element('Settings/section', [
|
||||
'sectionName' => $sectionName,
|
||||
'sectionContent' => $sectionContent,
|
||||
]);
|
||||
} else {
|
||||
$groupedContent[] = '';
|
||||
}
|
||||
if ($includeScrollspy) {
|
||||
if (!isLeaf($sectionContent)) {
|
||||
$scrollSpyContent[$sectionName] = array_filter( // only show grouped settings
|
||||
array_keys($sectionContent),
|
||||
function ($settingGroupName) use ($sectionContent) {
|
||||
return !isLeaf($sectionContent) && !empty($sectionContent[$settingGroupName]);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$contentHtml = implode('', $groupedContent);
|
||||
if ($includeScrollspy) {
|
||||
$scrollspyNav = $this->element('Settings/scrollspyNav', [
|
||||
'groupedSetting' => $scrollSpyContent
|
||||
]);
|
||||
}
|
||||
$mainPanelHeight = 'calc(100vh - 42px - 1rem - 56px - 38px - 1rem)';
|
||||
?>
|
||||
|
||||
<?php if ($includeScrollspy) : ?>
|
||||
<div class="d-flex">
|
||||
<div class="" style="flex: 0 0 10em;">
|
||||
<?= $scrollspyNav ?>
|
||||
</div>
|
||||
<div data-bs-spy="scroll" data-bs-target="#navbar-scrollspy-setting" data-bs-offset="25" style="height: <?= $mainPanelHeight ?>" class="p-3 overflow-auto position-relative flex-grow-1">
|
||||
<?= $contentHtml ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div>
|
||||
<?= $contentHtml ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
|
@ -1,4 +1,21 @@
|
|||
<?php
|
||||
if (!function_exists('isLeaf')) {
|
||||
function isLeaf($setting)
|
||||
{
|
||||
return !empty($setting['name']) && !empty($setting['type']);
|
||||
}
|
||||
}
|
||||
if (!function_exists('getResolvableID')) {
|
||||
function getResolvableID($sectionName, $panelName = false)
|
||||
{
|
||||
$id = sprintf('sp-%s', preg_replace('/(\.|\W)/', '_', h($sectionName)));
|
||||
if (!empty($panelName)) {
|
||||
$id .= '-' . preg_replace('/(\.|\W)/', '_', h($panelName));
|
||||
}
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
||||
$panelHTML = '';
|
||||
if (isLeaf($panelSettings)) {
|
||||
$singleSetting = $this->element('Settings/fieldGroup', [
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
<?php
|
||||
if (!function_exists('getResolvableID')) {
|
||||
function getResolvableID($sectionName, $panelName = false)
|
||||
{
|
||||
$id = sprintf('sp-%s', preg_replace('/(\.|\W)/', '_', h($sectionName)));
|
||||
if (!empty($panelName)) {
|
||||
$id .= '-' . preg_replace('/(\.|\W)/', '_', h($panelName));
|
||||
}
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<nav id="navbar-scrollspy-setting" class="navbar">
|
||||
<nav class="nav nav-pills flex-column">
|
||||
<?php foreach ($groupedSetting as $group => $sections): ?>
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
if (!function_exists('isLeaf')) {
|
||||
function isLeaf($setting)
|
||||
{
|
||||
return !empty($setting['name']) && !empty($setting['type']);
|
||||
}
|
||||
}
|
||||
if (!function_exists('getResolvableID')) {
|
||||
function getResolvableID($sectionName, $panelName = false)
|
||||
{
|
||||
$id = sprintf('sp-%s', preg_replace('/(\.|\W)/', '_', h($sectionName)));
|
||||
if (!empty($panelName)) {
|
||||
$id .= '-' . preg_replace('/(\.|\W)/', '_', h($panelName));
|
||||
}
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
||||
$sectionHtml = '';
|
||||
if (isLeaf($sectionContent)) {
|
||||
$sectionHtml .= $this->element('Settings/panel', [
|
||||
'sectionName' => $sectionName,
|
||||
'panelName' => $sectionName,
|
||||
'panelSettings' => $sectionContent,
|
||||
]);
|
||||
} else {
|
||||
if (count($sectionContent) > 0) {
|
||||
$sectionHtml .= sprintf('<h2 id="%s">%s</h2>', getResolvableID($sectionName), h($sectionName));
|
||||
}
|
||||
foreach ($sectionContent as $panelName => $panelSettings) {
|
||||
if (!empty($panelSettings)) {
|
||||
$sectionHtml .= $this->element('Settings/panel', [
|
||||
'sectionName' => $sectionName,
|
||||
'panelName' => $panelName,
|
||||
'panelSettings' => $panelSettings,
|
||||
]);
|
||||
} else {
|
||||
$sectionHtml .= '';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div>
|
||||
<?= $sectionHtml ?>
|
||||
</div>
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
$forSidebar = !empty($forSidebar);
|
||||
|
||||
$table = $this->Bootstrap->table([
|
||||
'hover' => false,
|
||||
], [
|
||||
'fields' => [
|
||||
['key' => 'label', 'label' => __('Label')],
|
||||
['key' => 'name', 'label' => __('Name')],
|
||||
['key' => 'url', 'label' => __('URL'), 'formatter' => function ($value, $row) {
|
||||
return sprintf('<span class="font-monospace">%s</span>', h($value));
|
||||
}],
|
||||
['key' => 'action', 'label' => __('Action'), 'formatter' => function ($value, $row, $index) {
|
||||
return $this->Bootstrap->button([
|
||||
'icon' => 'trash',
|
||||
'variant' => 'danger',
|
||||
'size' => 'sm',
|
||||
'params' => [
|
||||
'onclick' => sprintf('deleteBookmark(window.bookmarks[%s])', $index),
|
||||
]
|
||||
]);
|
||||
}],
|
||||
],
|
||||
'items' => $bookmarks,
|
||||
'caption' => empty($bookmarks) ? __('No bookmark saved') : ''
|
||||
]);
|
||||
?>
|
||||
|
||||
<?php if (!empty($forSidebar)) : ?>
|
||||
<li class="bookmarks">
|
||||
<?php foreach ($bookmarks as $parentName => $entry) : ?>
|
||||
<?= $this->element('layouts/sidebar/bookmark-entry', [
|
||||
'entry' => $entry,
|
||||
])
|
||||
?>
|
||||
<?php endforeach; ?>
|
||||
<?= $this->element('layouts/sidebar/bookmark-add') ?>
|
||||
</li>
|
||||
<?php else : ?>
|
||||
<div class="bookmark-table-container m-2">
|
||||
<button class="btn btn-primary mb-2" onclick="openSaveBookmarkModal()">
|
||||
<?= __('Create bookmark') ?>
|
||||
</button>
|
||||
<?= $table ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.bookmarks = <?= json_encode($bookmarks) ?>;
|
||||
</script>
|
||||
<?php endif; ?>
|
|
@ -2,6 +2,9 @@
|
|||
$controlParams = [
|
||||
'options' => $fieldData['options'],
|
||||
'empty' => $fieldData['empty'] ?? false,
|
||||
'value' => $fieldData['value'] ?? [],
|
||||
'multiple' => $fieldData['multiple'] ?? false,
|
||||
'disabled' => $fieldData['disabled'] ?? false,
|
||||
'class' => ($fieldData['class'] ?? '') . ' formDropdown form-select'
|
||||
];
|
||||
echo $this->FormFieldMassage->prepareFormElement($this->Form, $controlParams, $fieldData);
|
||||
|
|
|
@ -22,7 +22,8 @@
|
|||
|
||||
}
|
||||
$headersHtml .= sprintf(
|
||||
'<th scope="col">%s</th>',
|
||||
'<th scope="col" data-columnname="%s">%s</th>',
|
||||
h(\Cake\Utility\Inflector::variable(!empty($header['name']) ? $header['name'] : \Cake\Utility\Inflector::humanize($header['data_path']))),
|
||||
$header_data
|
||||
);
|
||||
}
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
'/genericElements/ListTopBar/scaffold',
|
||||
[
|
||||
'data' => $data['top_bar'],
|
||||
'table_data' => $data,
|
||||
'tableRandomValue' => $tableRandomValue
|
||||
]
|
||||
);
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
);
|
||||
}
|
||||
$rowHtml .= sprintf(
|
||||
'<td%s%s%s%s%s%s%s>%s</td>',
|
||||
'<td%s%s%s%s%s%s%s%s>%s</td>',
|
||||
(empty($field['id'])) ? '' : sprintf('id="%s"', $field['id']),
|
||||
(empty($field['class'])) ? '' : sprintf(' class="%s"', $field['class']),
|
||||
(empty($field['style'])) ? '' : sprintf(' style="%s"', $field['style']),
|
||||
|
@ -42,6 +42,10 @@
|
|||
h(implode(', ', $field['data_path'])) :
|
||||
(h($field['data_path']))
|
||||
),
|
||||
sprintf(
|
||||
' data-columnname="%s"',
|
||||
h(\Cake\Utility\Inflector::variable(!empty($field['name']) ? $field['name'] : \Cake\Utility\Inflector::humanize($field['data_path'])))
|
||||
),
|
||||
(empty($field['encode_raw_value']) || empty($field['data_path'])) ? '' : sprintf(' data-value="%s"', (h($this->Hash->extract($row, $field['data_path'])[0]))),
|
||||
(empty($field['ondblclick'])) ? '' : sprintf(' ondblclick="%s"', $field['ondblclick']),
|
||||
$valueField
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
if (empty($data['table_setting_id'])) {
|
||||
throw new Exception(__('`table_setting_id` must be set in order to use the `table_action` table topbar'));
|
||||
}
|
||||
$tableSettings = !empty($loggedUser->user_settings_by_name['ui.table_setting']['value']) ? json_decode($loggedUser->user_settings_by_name['ui.table_setting']['value'], true) : [];
|
||||
$tableSettings = !empty($tableSettings[$data['table_setting_id']]) ? $tableSettings[$data['table_setting_id']] : [];
|
||||
$compactDisplay = !empty($tableSettings['compact_display']);
|
||||
|
||||
$availableColumnsHtml = $this->element('/genericElements/ListTopBar/group_table_action/hiddenColumns', [
|
||||
'table_data' => $table_data,
|
||||
'tableSettings' => $tableSettings,
|
||||
'table_setting_id' => $data['table_setting_id'],
|
||||
]);
|
||||
$compactDisplayHtml = $this->element('/genericElements/ListTopBar/group_table_action/compactDisplay', [
|
||||
'table_data' => $table_data,
|
||||
'tableSettings' => $tableSettings,
|
||||
'table_setting_id' => $data['table_setting_id'],
|
||||
'compactDisplay' => $compactDisplay
|
||||
]);
|
||||
?>
|
||||
<?php if (!isset($data['requirement']) || $data['requirement']) : ?>
|
||||
<?php
|
||||
echo $this->Bootstrap->dropdownMenu([
|
||||
'dropdown-class' => 'ms-1',
|
||||
'alignment' => 'end',
|
||||
'direction' => 'down',
|
||||
'toggle-button' => [
|
||||
'icon' => 'sliders-h',
|
||||
'variant' => 'primary',
|
||||
],
|
||||
'submenu_alignment' => 'end',
|
||||
'submenu_direction' => 'start',
|
||||
'params' => [
|
||||
'data-table-random-value' => $tableRandomValue,
|
||||
'data-table_setting_id' => $data['table_setting_id'],
|
||||
],
|
||||
'menu' => [
|
||||
// [
|
||||
// 'text' => __('Group by'),
|
||||
// 'icon' => 'layer-group',
|
||||
// 'menu' => [
|
||||
// [
|
||||
// 'text' => 'fields to be grouped by', TODO:implement
|
||||
// ]
|
||||
// ],
|
||||
// ],
|
||||
[
|
||||
'text' => __('Show/hide columns'),
|
||||
'icon' => 'eye-slash',
|
||||
'keepOpen' => true,
|
||||
'menu' => [
|
||||
[
|
||||
'html' => $availableColumnsHtml,
|
||||
]
|
||||
],
|
||||
],
|
||||
[
|
||||
'html' => $compactDisplayHtml,
|
||||
]
|
||||
]
|
||||
]);
|
||||
?>
|
||||
<?php endif; ?>
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
$compactDisplayInputSeed = 'seed-' . mt_rand();
|
||||
?>
|
||||
|
||||
<label class="dropdown-item d-flex align-items-center cursor-pointer" href="#" for="<?= $compactDisplayInputSeed ?>">
|
||||
<span class="fa fa-text-height"></span>
|
||||
<span class="ms-2"><?= __('Compact display') ?></span>
|
||||
<input id="<?= $compactDisplayInputSeed ?>" type="checkbox" class="checkbox-compact-table form-check-input ms-auto" <?= $compactDisplay ? 'checked="checked"' : '' ?>>
|
||||
</label>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const debouncedCompactSaver = debounce(mergeAndSaveSettings, 2000)
|
||||
|
||||
$('#<?= $compactDisplayInputSeed ?>').change(function() {
|
||||
const $dropdownMenu = $(this).closest('.dropdown')
|
||||
const tableRandomValue = $dropdownMenu.attr('data-table-random-value')
|
||||
const $container = $dropdownMenu.closest('div[id^="table-container-"]')
|
||||
const $table = $container.find(`table[data-table-random-value="${tableRandomValue}"]`)
|
||||
const table_setting_id = $dropdownMenu.data('table_setting_id');
|
||||
toggleCompactState(this.checked, $table)
|
||||
let newTableSettings = {}
|
||||
newTableSettings[table_setting_id] = {
|
||||
'compact_display': this.checked
|
||||
}
|
||||
debouncedCompactSaver(table_setting_id, newTableSettings)
|
||||
})
|
||||
|
||||
function toggleCompactState(isCompact, $table) {
|
||||
if (isCompact) {
|
||||
$table.addClass('table-sm')
|
||||
} else {
|
||||
$table.removeClass('table-sm')
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
const $checkbox = $('#<?= $compactDisplayInputSeed ?>')
|
||||
const $dropdownMenu = $checkbox.closest('.dropdown')
|
||||
const tableRandomValue = $dropdownMenu.attr('data-table-random-value')
|
||||
const $container = $dropdownMenu.closest('div[id^="table-container-"]')
|
||||
const $table = $container.find(`table[data-table-random-value="${tableRandomValue}"]`)
|
||||
toggleCompactState($checkbox[0].checked, $table)
|
||||
})
|
||||
})()
|
||||
</script>
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
$tableSettings['hidden_column'] = $tableSettings['hidden_column'] ?? [];
|
||||
|
||||
$availableColumnsHtml = '';
|
||||
$availableColumns = [];
|
||||
foreach ($table_data['fields'] as $field) {
|
||||
$fieldName = !empty($field['name']) ? $field['name'] : \Cake\Utility\Inflector::humanize($field['data_path']);
|
||||
$isVisible = !in_array(h(\Cake\Utility\Inflector::variable($fieldName)), $tableSettings['hidden_column']);
|
||||
$availableColumns[] = $fieldName;
|
||||
$availableColumnsHtml .= sprintf(
|
||||
'<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="" id="columnCheck-%s" data-columnname="%s" %s>
|
||||
<label class="form-check-label w-100 cursor-pointer" for="columnCheck-%s">
|
||||
%s
|
||||
</label>
|
||||
</div>',
|
||||
h(\Cake\Utility\Inflector::variable($fieldName)),
|
||||
h(\Cake\Utility\Inflector::variable($fieldName)),
|
||||
$isVisible ? 'checked' : '',
|
||||
h(\Cake\Utility\Inflector::variable($fieldName)),
|
||||
h($fieldName)
|
||||
);
|
||||
}
|
||||
|
||||
$availableColumnsHtml = $this->Bootstrap->genNode('form', [
|
||||
'class' => ['visible-column-form', 'px-2 py-1'],
|
||||
], $availableColumnsHtml);
|
||||
echo $availableColumnsHtml;
|
||||
?>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const debouncedHiddenColumnSaver = debounce(mergeAndSaveSettings, 2000)
|
||||
$('form.visible-column-form').find('input').change(function() {
|
||||
const $dropdownMenu = $(this).closest(`[data-table-random-value]`)
|
||||
const tableRandomValue = $dropdownMenu.attr('data-table-random-value')
|
||||
const $container = $dropdownMenu.closest('div[id^="table-container-"]')
|
||||
const $table = $container.find(`table[data-table-random-value="${tableRandomValue}"]`)
|
||||
const table_setting_id = $dropdownMenu.data('table_setting_id');
|
||||
toggleColumn(this.getAttribute('data-columnname'), this.checked, $table)
|
||||
let tableSettings = {}
|
||||
tableSettings[table_setting_id] = genTableSettings($container)
|
||||
debouncedHiddenColumnSaver(table_setting_id, tableSettings)
|
||||
})
|
||||
|
||||
function toggleColumn(columnName, isVisible, $table) {
|
||||
if (isVisible) {
|
||||
$table.find(`th[data-columnname="${columnName}"],td[data-columnname="${columnName}"]`).show()
|
||||
} else {
|
||||
$table.find(`th[data-columnname="${columnName}"],td[data-columnname="${columnName}"]`).hide()
|
||||
}
|
||||
}
|
||||
|
||||
function genTableSettings($container) {
|
||||
let tableSetting = {};
|
||||
const $hiddenColumns = $container.find('form.visible-column-form').find('input').not(':checked')
|
||||
const hiddenColumns = Array.from($hiddenColumns.map(function() {
|
||||
return $(this).data('columnname')
|
||||
}))
|
||||
tableSetting['hidden_column'] = hiddenColumns
|
||||
return tableSetting
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
const $form = $('form.visible-column-form')
|
||||
const $checkboxes = $form.find('input').not(':checked')
|
||||
const $dropdownMenu = $form.closest('.dropdown')
|
||||
const tableRandomValue = $dropdownMenu.attr('data-table-random-value')
|
||||
const $container = $dropdownMenu.closest('div[id^="table-container-"]')
|
||||
const $table = $container.find(`table[data-table-random-value="${tableRandomValue}"]`)
|
||||
$checkboxes.each(function() {
|
||||
toggleColumn(this.getAttribute('data-columnname'), this.checked, $table)
|
||||
})
|
||||
})
|
||||
})()
|
||||
</script>
|
|
@ -1,8 +1,13 @@
|
|||
<?php
|
||||
$groups = '';
|
||||
$hasGroupSearch = false;
|
||||
|
||||
foreach ($data['children'] as $group) {
|
||||
$groups .= $this->element('/genericElements/ListTopBar/group_' . (empty($group['type']) ? 'simple' : h($group['type'])), array('data' => $group, 'tableRandomValue' => $tableRandomValue));
|
||||
$groups .= $this->element('/genericElements/ListTopBar/group_' . (empty($group['type']) ? 'simple' : h($group['type'])), array(
|
||||
'data' => $group,
|
||||
'tableRandomValue' => $tableRandomValue,
|
||||
'table_data' => $table_data,
|
||||
));
|
||||
$hasGroupSearch = $hasGroupSearch || (!empty($group['type']) && $group['type'] == 'search');
|
||||
}
|
||||
$tempClass = "btn-toolbar";
|
||||
|
|
|
@ -1,27 +1,37 @@
|
|||
<?php
|
||||
use Cake\Core\Configure;
|
||||
use Cake\Routing\Router;
|
||||
|
||||
$controller = $this->request->getParam('controller');
|
||||
$action = $this->request->getParam('action');
|
||||
$curentPath = "{$controller}{$action}";
|
||||
use Cake\Core\Configure;
|
||||
use Cake\Routing\Router;
|
||||
|
||||
$breadcrumbLinks = '';
|
||||
$breadcrumbAction = '';
|
||||
$this->Breadcrumbs->setTemplates([
|
||||
$controller = $this->request->getParam('controller');
|
||||
$action = $this->request->getParam('action');
|
||||
$curentPath = "{$controller}{$action}";
|
||||
$entity = !empty($entity) ? $entity : [];
|
||||
|
||||
$breadcrumbLinks = '';
|
||||
$breadcrumbAction = '';
|
||||
$this->Breadcrumbs->setTemplates([
|
||||
'wrapper' => sprintf(
|
||||
'<nav class="header-breadcrumb d-lg-block d-none"{{attrs}}><ol class="">{{content}}</ol></nav>'
|
||||
),
|
||||
'item' => '<li class="header-breadcrumb-item"{{attrs}}><i class="{{icon}} me-1"></i><a class="{{linkClass}}" href="{{url}}"{{innerAttrs}}>{{title}}</a></li>{{separator}}',
|
||||
'itemWithoutLink' => '<li class="header-breadcrumb-item"{{attrs}}><span{{innerAttrs}}>{{title}}</span></li>{{separator}}',
|
||||
'separator' => '<li class="header-breadcrumb-separator"{{attrs}}><span{{innerAttrs}}><i class="fa fa-sm fa-angle-right"></i></span></li>'
|
||||
]);
|
||||
]);
|
||||
|
||||
if (!empty($breadcrumb)) {
|
||||
if (!empty($breadcrumb)) {
|
||||
foreach ($breadcrumb as $i => $entry) {
|
||||
if (!empty($entry['textGetter'])) {
|
||||
if (is_array($entry['textGetter']) && !empty($entry['textGetter']['path'])) {
|
||||
$data = !empty(${$entry['textGetter']['varname']}) ? ${$entry['textGetter']['varname']} : $entity;
|
||||
$entry['label'] = Cake\Utility\Hash::get($data, $entry['textGetter']['path']);
|
||||
} else {
|
||||
$entry['label'] = Cake\Utility\Hash::get($entity, $entry['textGetter']);
|
||||
}
|
||||
}
|
||||
if (empty($entry['label'])) {
|
||||
$entry['label'] = "[{$entry['textGetter']}]";
|
||||
}
|
||||
if (!empty($entry['url_vars'])) {
|
||||
$entry['url'] = $this->DataFromPath->buildStringFromDataPath($entry['url'], $entity, $entry['url_vars']);
|
||||
}
|
||||
|
@ -34,17 +44,24 @@
|
|||
]);
|
||||
}
|
||||
|
||||
$lastCrumb = $breadcrumb[count($breadcrumb)-1];
|
||||
$lastCrumb = $breadcrumb[count($breadcrumb) - 1];
|
||||
|
||||
if (!empty($lastCrumb['links'])) {
|
||||
// dd($lastCrumb['links']);
|
||||
foreach ($lastCrumb['links'] as $i => $linkEntry) {
|
||||
$active = $linkEntry['route_path'] == $lastCrumb['route_path'];
|
||||
if (!empty($linkEntry['url_vars'])) {
|
||||
$linkEntry['url'] = $this->DataFromPath->buildStringFromDataPath($linkEntry['url'], $entity, $linkEntry['url_vars']);
|
||||
}
|
||||
$breadcrumbLinks .= sprintf('<a class="btn btn-%s btn-sm text-nowrap" role="button" href="%s">%s</a>',
|
||||
if (!empty($linkEntry['selfLink'])) {
|
||||
$url = Router::url(null);
|
||||
} else {
|
||||
$url = Router::url($linkEntry['url']);
|
||||
}
|
||||
$breadcrumbLinks .= sprintf(
|
||||
'<a class="btn btn-%s btn-sm text-nowrap" role="button" href="%s">%s</a>',
|
||||
$active ? 'secondary' : 'outline-secondary',
|
||||
Router::url($linkEntry['url']),
|
||||
$url,
|
||||
h($linkEntry['label'])
|
||||
);
|
||||
}
|
||||
|
@ -54,51 +71,43 @@
|
|||
if (!empty($actionEntry['url_vars'])) {
|
||||
$actionEntry['url'] = $this->DataFromPath->buildStringFromDataPath($actionEntry['url'], $entity, $actionEntry['url_vars']);
|
||||
}
|
||||
$breadcrumbAction .= sprintf('<a class="dropdown-item" href="%s"><i class="me-1 %s"></i>%s</a>',
|
||||
$breadcrumbAction .= sprintf(
|
||||
'<a class="dropdown-item" href="%s"><i class="me-1 %s"></i>%s</a>',
|
||||
Router::url($actionEntry['url']),
|
||||
!empty($entry['icon']) ? $this->FontAwesome->getClass(h($actionEntry['icon'])) : '',
|
||||
!empty($actionEntry['icon']) ? $this->FontAwesome->getClass(h($actionEntry['icon'])) : '',
|
||||
h($actionEntry['label'])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<?php
|
||||
echo $this->Breadcrumbs->render(
|
||||
echo $this->Breadcrumbs->render(
|
||||
[],
|
||||
['separator' => '']
|
||||
);
|
||||
);
|
||||
?>
|
||||
|
||||
<?php if (!empty($breadcrumbLinks) && !empty($breadcrumbAction)): ?>
|
||||
<div class="breadcrumb-link-container position-absolute end-0 d-flex">
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($breadcrumbLinks) || !empty($breadcrumbAction)) : ?>
|
||||
<div class="breadcrumb-link-container position-absolute end-0 d-flex">
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($breadcrumbLinks)): ?>
|
||||
<?php if (!empty($breadcrumbLinks)) : ?>
|
||||
<div class="header-breadcrumb-children d-none d-md-flex btn-group">
|
||||
<?= $breadcrumbLinks ?>
|
||||
<?php if (!empty($breadcrumbAction)) : ?>
|
||||
<a class="btn btn-primary btn-sm dropdown-toggle" href="#" role="button" id="dropdownMenuBreadcrumbAction" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<?= __('Actions') ?>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="dropdownMenuBreadcrumbAction">
|
||||
<?= $breadcrumbAction ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($breadcrumbLinks) || !empty($breadcrumbAction)) : ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($breadcrumbAction) && false): ?>
|
||||
<div class="header-breadcrumb-actions dropdown d-flex align-items-center">
|
||||
<a class="btn btn-primary btn-sm dropdown-toggle" href="#" role="button" id="dropdownMenuBreadcrumbAction" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<?= __('Actions') ?>
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuBreadcrumbAction">
|
||||
<?= $breadcrumbAction ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($breadcrumbLinks) && !empty($breadcrumbAction)): ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
|
@ -15,13 +15,16 @@ use Cake\Routing\Router;
|
|||
</div>
|
||||
</h6>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="<?= Router::url(['controller' => 'users', 'action' => 'view', 'plugin' => null]) ?>">
|
||||
<a class="dropdown-item" href="<?= Router::url(['controller' => 'users', 'action' => 'view', 'plugin' => null, h($this->request->getAttribute('identity')['id'])]) ?>">
|
||||
<i class="me-1 <?= $this->FontAwesome->getClass('user-circle') ?>"></i>
|
||||
<?= __('My Account') ?>
|
||||
</a>
|
||||
<a class="dropdown-item" href="<?= Router::url(['controller' => 'users', 'action' => 'userSettings', 'plugin' => null]) ?>">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="<?= Router::url(['controller' => 'users', 'action' => 'settings', 'plugin' => null, h($this->request->getAttribute('identity')['id'])]) ?>"
|
||||
>
|
||||
<i class="me-1 <?= $this->FontAwesome->getClass('user-cog') ?>"></i>
|
||||
<?= __('Settings') ?>
|
||||
<?= __('Account Settings') ?>
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item dropdown-item-outline-danger" href="<?= Router::url(['controller' => 'users', 'action' => 'logout', 'plugin' => null]) ?>">
|
||||
|
|
|
@ -1,20 +1,33 @@
|
|||
<?php
|
||||
$bookmarkIncluded = $loggedUser->user_settings_by_name_with_fallback['ui.sidebar.include_bookmarks']['value'];
|
||||
?>
|
||||
<div class="sidebar-wrapper d-flex flex-column">
|
||||
<div class="sidebar-scroll">
|
||||
<div class="sidebar-content">
|
||||
<ul class="sidebar-elements">
|
||||
<?php foreach ($menu as $category => $categorized): ?>
|
||||
<?php foreach ($menu as $category => $categorized) : ?>
|
||||
<?php if ($category == '__bookmarks') : ?>
|
||||
<?php if ($bookmarkIncluded) : ?>
|
||||
<?= $this->element('layouts/sidebar/category', ['label' => __('Bookmarks'), 'class' => 'bookmark-categ']) ?>
|
||||
<?= $this->element('UserSettings/saved-bookmarks', [
|
||||
'bookmarks' => $categorized,
|
||||
'forSidebar' => true,
|
||||
]) ?>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<?= $this->element('layouts/sidebar/category', ['label' => $category]) ?>
|
||||
<?php foreach ($categorized as $parentName => $parent): ?>
|
||||
<?php foreach ($categorized as $parentName => $parent) : ?>
|
||||
<?= $this->element('layouts/sidebar/entry', [
|
||||
'parent' => $parent,
|
||||
])
|
||||
?>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<span class="lock-sidebar align-self-center mt-auto w-100 d-none d-sm-block" onclick="$('.sidebar').toggleClass('expanded')">
|
||||
<a type="button" class="btn btn-sm w-100"></a>
|
||||
<span class="lock-sidebar align-self-center mt-auto w-100 d-none d-sm-block">
|
||||
<a type="button" class="btn-lock-sidebar btn btn-sm w-100"></a>
|
||||
</span>
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
echo $this->Bootstrap->button([
|
||||
'nodeType' => 'a',
|
||||
'icon' => 'plus',
|
||||
'title' => __('Add new bookmark'),
|
||||
'variant' => 'primary',
|
||||
'size' => 'sm',
|
||||
'class' => 'mb-1',
|
||||
'params' => [
|
||||
'id' => 'btn-add-bookmark',
|
||||
]
|
||||
]);
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
use Cake\Routing\Router;
|
||||
|
||||
$seed = 'sb-' . mt_rand();
|
||||
$icon = $entry['icon'] ?? '';
|
||||
$label = $entry['label'] ?? '';
|
||||
$name = $entry['name'] ?? '';
|
||||
$active = false;
|
||||
|
||||
$url = $entry['url'];
|
||||
|
||||
$currentURL = Router::url(null);
|
||||
if ($url == $currentURL) {
|
||||
$active = true;
|
||||
}
|
||||
|
||||
echo $this->Bootstrap->button([
|
||||
'nodeType' => 'a',
|
||||
'text' => h($label),
|
||||
'title' => h($name),
|
||||
'variant' => 'dark',
|
||||
'outline' => !$active,
|
||||
'size' => 'sm',
|
||||
'icon' => h($icon),
|
||||
'class' => ['mb-1'],
|
||||
'params' => [
|
||||
'href' => h($url),
|
||||
]
|
||||
]);
|
||||
?>
|
|
@ -1,4 +1,4 @@
|
|||
<li class="category text-muted">
|
||||
<li class="category text-muted <?= !empty($class) ? h($class) : '' ?>">
|
||||
<span class="category-label"><?= h($label) ?></span>
|
||||
<span class="category-divider"><hr /></span>
|
||||
</li>
|
||||
|
|
|
@ -29,7 +29,12 @@
|
|||
?>
|
||||
|
||||
<li class="<?= !empty($children) ? 'parent collapsed' : '' ?>">
|
||||
<a class="sidebar-link <?= !empty($children) ? 'collapsed' : '' ?> <?= $active ? 'active' : '' ?> <?= $hasActiveChild ? 'have-active-child' : '' ?>" href="<?= h($url) ?>" <?= !empty($children) ? 'data-bs-toggle="collapse"' : '' ?> <?= $hasActiveChild ? 'aria-expanded="true"' : '' ?>>
|
||||
<a
|
||||
class="sidebar-link <?= !empty($children) ? 'collapsed' : '' ?> <?= $active ? 'active' : '' ?> <?= $hasActiveChild ? 'have-active-child' : '' ?>"
|
||||
href="<?= h($url) ?>"
|
||||
<?= !empty($children) ? 'data-bs-toggle="collapse"' : '' ?>
|
||||
<?= $hasActiveChild ? 'aria-expanded="true"' : '' ?>
|
||||
>
|
||||
<i class="sidebar-icon <?= $this->FontAwesome->getClass($icon) ?>"></i>
|
||||
<span class="text"><?= h($label) ?></span>
|
||||
</a>
|
||||
|
|
|
@ -2,18 +2,19 @@
|
|||
$variationIcon = '';
|
||||
$variationClass = '';
|
||||
if ($variation == 0) {
|
||||
$variationIcon = 'minus';
|
||||
$variationIcon = $this->FontAwesome->getClass('minus');
|
||||
} elseif ($variation > 0) {
|
||||
$variationIcon = 'arrow-up';
|
||||
$variationIcon = 'trends-arrow-up-white fs-6';
|
||||
$variationClass = 'bg-success';
|
||||
} else {
|
||||
$variationIcon = 'arrow-down';
|
||||
$variationIcon = 'trends-arrow-up-white fs-6 fa-rotate-180 fa-flip-vertical';
|
||||
$variationClass = 'bg-danger';
|
||||
}
|
||||
|
||||
$variationHtml = sprintf('<div class="badge %s fw-bold"><span class="%s me-2"></span>%s</div>',
|
||||
$variationHtml = sprintf(
|
||||
'<div class="badge %s fw-bold"><span class="%s me-2 align-middle"></span>%s</div>',
|
||||
$variationClass,
|
||||
$this->FontAwesome->getClass($variationIcon),
|
||||
$variationIcon,
|
||||
!empty($variation) ? h($variation) : ''
|
||||
);
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
use Cake\Core\Configure;
|
||||
|
||||
$cakeDescription = 'Cerebrate';
|
||||
|
||||
$sidebarOpen = $loggedUser->user_settings_by_name_with_fallback['ui.sidebar.expanded']['value'];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
@ -41,6 +43,7 @@ $cakeDescription = 'Cerebrate';
|
|||
<?= $this->Html->script('bootstrap-helper.js') ?>
|
||||
<?= $this->Html->script('api-helper.js') ?>
|
||||
<?= $this->Html->script('select2.min.js') ?>
|
||||
<?= $this->Html->script('table-settings.js') ?>
|
||||
<?= $this->Html->script('CodeMirror/codemirror.js') ?>
|
||||
<?= $this->Html->script('CodeMirror/mode/javascript/javascript') ?>
|
||||
<?= $this->Html->script('CodeMirror/addon/hint/show-hint') ?>
|
||||
|
@ -72,7 +75,7 @@ $cakeDescription = 'Cerebrate';
|
|||
<header class="navbar top-navbar navbar-dark">
|
||||
<?= $this->element('layouts/header') ?>
|
||||
</header>
|
||||
<nav id="app-sidebar" class="collapse d-sm-block sidebar">
|
||||
<nav id="app-sidebar" class="collapse d-sm-block sidebar <?= !empty($sidebarOpen) ? 'expanded' : '' ?>">
|
||||
<?= $this->element('layouts/sidebar') ?>
|
||||
</nav>
|
||||
<main role="main" class="content">
|
||||
|
|
|
@ -184,6 +184,13 @@ main.content {
|
|||
|
||||
.global-search-container #globalSearch {
|
||||
padding-right: 26px;
|
||||
width: 280px;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, width 0.3s;
|
||||
-webkit-transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, width 0.3s;
|
||||
}
|
||||
|
||||
.global-search-container #globalSearch:focus {
|
||||
width: 380px !important;
|
||||
}
|
||||
|
||||
.top-navbar .global-search-container .search-input-container > i.icon {
|
||||
|
@ -210,6 +217,11 @@ main.content {
|
|||
.sidebar-scroll {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.sidebar-scroll:hover {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
|
@ -221,26 +233,63 @@ ul.sidebar-elements {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar .lock-sidebar {
|
||||
-webkit-box-shadow: 0px -1px 5px 1px rgb(0 0 0 / 8%);
|
||||
box-shadow: 0px -1px 5px 1px rgb(0 0 0 / 8%);
|
||||
}
|
||||
|
||||
.sidebar .lock-sidebar > a::before {
|
||||
content: "\f101";
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900;
|
||||
transition: margin-left 0.1s ease-out;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar.expanded .lock-sidebar > a::before {
|
||||
content: "\f100";
|
||||
}
|
||||
|
||||
.sidebar .lock-sidebar:hover > a::before {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.sidebar.expanded .lock-sidebar:hover > a::before {
|
||||
margin-left: -0.5rem;
|
||||
}
|
||||
|
||||
/* sidebar entry */
|
||||
ul.sidebar-elements > li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
ul.sidebar-elements > li > a[data-href] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul.sidebar-elements li.bookmarks {
|
||||
padding: 0 calc((var(--sidebar-width-collapsed) - 25px) / 2);
|
||||
}
|
||||
|
||||
.sidebar.expanded ul.sidebar-elements li > a.sidebar-link,
|
||||
.sidebar:hover ul.sidebar-elements li > a.sidebar-link {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sidebar ul.sidebar-elements li.bookmark-categ,
|
||||
.sidebar ul.sidebar-elements li.bookmarks {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar.expanded ul.sidebar-elements li.bookmark-categ,
|
||||
.sidebar:hover ul.sidebar-elements li.bookmark-categ {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sidebar.expanded ul.sidebar-elements li.bookmarks,
|
||||
.sidebar:hover ul.sidebar-elements li.bookmarks {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
ul.sidebar-elements li > a.sidebar-link {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
@ -476,11 +525,13 @@ ul.sidebar-elements > li.category > span.category-divider > hr {
|
|||
mask-repeat: no-repeat;
|
||||
mask-size: contain, cover;
|
||||
mask-composite: source-out;
|
||||
mask-composite: subtract;
|
||||
-webkit-mask-image: url(/img/icon-composition/sheet-all.svg), url(/img/icon-composition/z.svg);
|
||||
-webkit-mask-position: 0 0, 2.4px calc((var(--navbar-height) - 10px) / 2);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-size: contain, cover;
|
||||
-webkit-mask-composite: source-out;
|
||||
-webkit-mask-composite: subtract;
|
||||
transition-timing-function: ease-out;
|
||||
transition-duration: 0.2s;
|
||||
transition-property: -webkit-mask-position;
|
||||
|
|
|
@ -58,6 +58,10 @@
|
|||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link-unstyled, .link-unstyled:link, .link-unstyled:hover {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
|
@ -98,15 +102,16 @@ input[type="checkbox"]:disabled.change-cursor {
|
|||
}
|
||||
|
||||
.grow-on-hover {
|
||||
-webkit-transition: -webkit-transform 0.5s cubic-bezier(0, 0.90, 0.35, 1.00);
|
||||
-moz-transition: -moz-transform 0.5s cubic-bezier(0, 0.90, 0.35, 1.00);
|
||||
transition: all .2s cubic-bezier(0, 0.90, 0.35, 1.00);
|
||||
-webkit-transition: all 0.2s ease-out;
|
||||
-moz-transition: all 0.2s ease-out;
|
||||
transition: all .2s ease-out;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.grow-on-hover:hover {
|
||||
-webkit-transform: translateZ(0) scale(1.02, 1.02);
|
||||
-moz-transform: translateZ(0) scale(1.02, 1.02);
|
||||
transform: translateZ(0) scale(1.02, 1.02);
|
||||
-webkit-transform: translateZ(0) scale(1.05);
|
||||
-moz-transform: translateZ(0) scale(1.05);
|
||||
transform: translateZ(0) scale(1.05);
|
||||
box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 15%) !important;
|
||||
}
|
||||
|
||||
|
@ -127,3 +132,39 @@ input[type="checkbox"]:disabled.change-cursor {
|
|||
50% { opacity: 0.3; }
|
||||
100% { transform: translateX(0%); opacity: 1; }
|
||||
}
|
||||
|
||||
.trends-arrow-up-black {
|
||||
display: inline-flex;
|
||||
min-width: 1em;
|
||||
min-height: 0.71em;
|
||||
position: relative;
|
||||
}
|
||||
.trends-arrow-up-black::before {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: " ";
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8' standalone='no'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' height='223.995' width='383.98752' viewBox='0 0 383.98752 223.995' role='img'%3E%3Cpath d='m 367.9975,0 h -118.06 c -21.38,0 -32.09,25.85 -16.97,40.97 l 32.4,32.4 -73.37,73.38 -73.37,-73.37 c -12.5,-12.5 -32.76,-12.5 -45.25,0 l -68.69,68.69 c -6.25,6.25 -6.25,16.38 0,22.63 l 22.62,22.62 c 6.25,6.25 16.38,6.25 22.63,0 l 46.06,-46.07 73.37,73.37 c 12.5,12.5 32.76,12.5 45.25,0 l 96,-96 32.4,32.4 c 15.12,15.12 40.97,4.41 40.97,-16.97 V 16 c 0.01,-8.84 -7.15,-16 -15.99,-16 z' /%3E%3C/svg%3E%0A");.97,-16.97 V 16 c 0.01,-8.84 -7.15,-16 -15.99,-16 z' /%3E%3C/svg%3E%0A");
|
||||
}
|
||||
|
||||
.trends-arrow-up-white {
|
||||
display: inline-flex;
|
||||
min-width: 1em;
|
||||
min-height: 0.71em;
|
||||
position: relative;
|
||||
}
|
||||
.trends-arrow-up-white::before {
|
||||
background-color: #fff;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: " ";
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
mask-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8' standalone='no'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' height='223.995' width='383.98752' viewBox='0 0 383.98752 223.995' role='img'%3E%3Cpath d='m 367.9975,0 h -118.06 c -21.38,0 -32.09,25.85 -16.97,40.97 l 32.4,32.4 -73.37,73.38 -73.37,-73.37 c -12.5,-12.5 -32.76,-12.5 -45.25,0 l -68.69,68.69 c -6.25,6.25 -6.25,16.38 0,22.63 l 22.62,22.62 c 6.25,6.25 16.38,6.25 22.63,0 l 46.06,-46.07 73.37,73.37 c 12.5,12.5 32.76,12.5 45.25,0 l 96,-96 32.4,32.4 c 15.12,15.12 40.97,4.41 40.97,-16.97 V 16 c 0.01,-8.84 -7.15,-16 -15.99,-16 z' /%3E%3C/svg%3E%0A");
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8' standalone='no'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' height='223.995' width='383.98752' viewBox='0 0 383.98752 223.995' role='img'%3E%3Cpath d='m 367.9975,0 h -118.06 c -21.38,0 -32.09,25.85 -16.97,40.97 l 32.4,32.4 -73.37,73.38 -73.37,-73.37 c -12.5,-12.5 -32.76,-12.5 -45.25,0 l -68.69,68.69 c -6.25,6.25 -6.25,16.38 0,22.63 l 22.62,22.62 c 6.25,6.25 16.38,6.25 22.63,0 l 46.06,-46.07 73.37,73.37 c 12.5,12.5 32.76,12.5 45.25,0 l 96,-96 32.4,32.4 c 15.12,15.12 40.97,4.41 40.97,-16.97 V 16 c 0.01,-8.84 -7.15,-16 -15.99,-16 z' /%3E%3C/svg%3E%0A");
|
||||
}
|
|
@ -308,7 +308,6 @@ class Toaster {
|
|||
return $(this).is($toast)
|
||||
});
|
||||
if (hoveredElements.length > 0) {
|
||||
console.log('Toast hovered. Not hidding')
|
||||
evt.preventDefault()
|
||||
setTimeout(() => {
|
||||
$toast.toast('hide')
|
||||
|
@ -899,6 +898,9 @@ class OverlayFactory {
|
|||
const boundingRect = this.$node[0].getBoundingClientRect()
|
||||
this.$overlayWrapper.css('min-height', Math.max(boundingRect.height, 20))
|
||||
this.$overlayWrapper.css('min-width', Math.max(boundingRect.width, 20))
|
||||
if (this.$node.hasClass('row')) {
|
||||
this.$overlayWrapper.addClass('row')
|
||||
}
|
||||
}
|
||||
this.$overlayContainer = $(OverlayFactory.overlayContainer)
|
||||
this.$overlayBg = $(OverlayFactory.overlayBg)
|
||||
|
|
|
@ -152,14 +152,147 @@ function focusSearchResults(evt) {
|
|||
}
|
||||
}
|
||||
|
||||
function saveUserSetting(statusNode, settingName, settingValue) {
|
||||
const url = window.saveSettingURL
|
||||
const data = {
|
||||
name: settingName,
|
||||
value: settingValue,
|
||||
}
|
||||
const APIOptions = {
|
||||
statusNode: statusNode,
|
||||
}
|
||||
return AJAXApi.quickFetchAndPostForm(url, data, APIOptions)
|
||||
}
|
||||
|
||||
function openSaveBookmarkModal(bookmark_url = '') {
|
||||
const url = '/user-settings/saveBookmark';
|
||||
UI.submissionModal(url).then(([modalFactory, ajaxApi]) => {
|
||||
const $input = modalFactory.$modal.find('input[name="bookmark_url"]')
|
||||
$input.val(bookmark_url)
|
||||
})
|
||||
}
|
||||
|
||||
function deleteBookmark(bookmark, forSidebar=false) {
|
||||
const url = '/user-settings/deleteBookmark'
|
||||
AJAXApi.quickFetchAndPostForm(url, {
|
||||
bookmark_name: bookmark.name,
|
||||
bookmark_url: bookmark.url,
|
||||
}, {
|
||||
provideFeedback: true,
|
||||
statusNode: $('.bookmark-table-container'),
|
||||
}).then((apiResult) => {
|
||||
const url = `/userSettings/getBookmarks/${forSidebar ? '1' : '0'}`
|
||||
UI.reload(url, $('.bookmark-table-container').parent())
|
||||
const theToast = UI.toast({
|
||||
variant: 'success',
|
||||
title: apiResult.message,
|
||||
bodyHtml: $('<div/>').append(
|
||||
$('<span/>').text('Cancel deletion operation.'),
|
||||
$('<button/>').addClass(['btn', 'btn-primary', 'btn-sm', 'ms-3']).text('Restore bookmark').click(function () {
|
||||
const urlRestore = '/user-settings/saveBookmark'
|
||||
AJAXApi.quickFetchAndPostForm(urlRestore, {
|
||||
bookmark_label: bookmark.label,
|
||||
bookmark_name: bookmark.name,
|
||||
bookmark_url: bookmark.url,
|
||||
}, {
|
||||
provideFeedback: true,
|
||||
statusNode: $('.bookmark-table-container')
|
||||
}).then(() => {
|
||||
const url = `/userSettings/getBookmarks/${forSidebar ? '1' : '0'}`
|
||||
UI.reload(url, $('.bookmark-table-container').parent())
|
||||
})
|
||||
}),
|
||||
),
|
||||
})
|
||||
}).catch((e) => { })
|
||||
}
|
||||
|
||||
function overloadBSDropdown() {
|
||||
// Inspired from https://jsfiddle.net/dallaslu/mvk4uhzL/
|
||||
(function ($bs) {
|
||||
const CLASS_NAME_HAS_CHILD = 'has-child-dropdown-show';
|
||||
const CLASS_NAME_KEEP_OPEN = 'keep-dropdown-show';
|
||||
|
||||
$bs.Dropdown.prototype.toggle = function (_orginal) {
|
||||
return function () {
|
||||
document.querySelectorAll('.' + CLASS_NAME_HAS_CHILD).forEach(function (e) {
|
||||
e.classList.remove(CLASS_NAME_HAS_CHILD);
|
||||
});
|
||||
let dd = this._element.closest('.dropdown')
|
||||
if (dd !== null) {
|
||||
dd = dd.parentNode.closest('.dropdown');
|
||||
for (; dd && dd !== document; dd = dd.parentNode.closest('.dropdown')) {
|
||||
dd.classList.add(CLASS_NAME_HAS_CHILD);
|
||||
}
|
||||
|
||||
if (this._element.classList.contains('open-form')) {
|
||||
const openFormId = this._element.getAttribute('data-open-form-id')
|
||||
document.querySelectorAll('.' + CLASS_NAME_KEEP_OPEN).forEach(function (e) {
|
||||
e.classList.remove(CLASS_NAME_KEEP_OPEN);
|
||||
});
|
||||
let dd = this._element.closest('.dropdown')
|
||||
dd.classList.add(CLASS_NAME_KEEP_OPEN);
|
||||
dd.setAttribute('data-open-form-id', openFormId)
|
||||
dd = dd.parentNode.closest('.dropdown');
|
||||
for (; dd && dd !== document; dd = dd.parentNode.closest('.dropdown')) {
|
||||
dd.setAttribute('data-open-form-id', openFormId)
|
||||
dd.classList.add(CLASS_NAME_KEEP_OPEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _orginal.call(this);
|
||||
}
|
||||
}($bs.Dropdown.prototype.toggle);
|
||||
|
||||
document.querySelectorAll('.dropdown').forEach(function (dd) {
|
||||
dd.addEventListener('hide.bs.dropdown', function (e) {
|
||||
if (this.classList.contains(CLASS_NAME_HAS_CHILD)) {
|
||||
this.classList.remove(CLASS_NAME_HAS_CHILD);
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (e.clickEvent !== undefined) {
|
||||
let dd = e.clickEvent.target.closest('.dropdown')
|
||||
if (dd !== null) {
|
||||
if (dd.classList.contains('keep-dropdown-show')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
e.stopPropagation(); // do not need pop in multi level mode
|
||||
});
|
||||
});
|
||||
})(bootstrap);
|
||||
}
|
||||
|
||||
var UI
|
||||
$(document).ready(() => {
|
||||
if (typeof UIFactory !== "undefined") {
|
||||
UI = new UIFactory()
|
||||
}
|
||||
overloadBSDropdown();
|
||||
|
||||
const debouncedGlobalSearch = debounce(performGlobalSearch, 400)
|
||||
$('#globalSearch')
|
||||
.keydown(debouncedGlobalSearch)
|
||||
.keydown(focusSearchResults);
|
||||
|
||||
$('.lock-sidebar a.btn-lock-sidebar').click(() => {
|
||||
const $sidebar = $('.sidebar')
|
||||
let expanded = $sidebar.hasClass('expanded');
|
||||
if (expanded) {
|
||||
$sidebar.removeClass('expanded')
|
||||
} else {
|
||||
$sidebar.addClass('expanded')
|
||||
}
|
||||
const settingName = 'ui.sidebar.expanded';
|
||||
const url = `/user-settings/setSetting/${settingName}`
|
||||
AJAXApi.quickFetchAndPostForm(url, {
|
||||
value: expanded ? 0 : 1
|
||||
}, { provideFeedback: false})
|
||||
})
|
||||
|
||||
$('.sidebar #btn-add-bookmark').click(() => {
|
||||
openSaveBookmarkModal(window.location.pathname)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
const variantFromSeverity = {
|
||||
'critical': 'danger',
|
||||
'warning': 'warning',
|
||||
'info': 'info',
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
if (
|
||||
variantFromSeverity === undefined ||
|
||||
window.settingsFlattened === undefined ||
|
||||
window.saveSettingURL === undefined
|
||||
) {
|
||||
console.error('`settingFlatenned` and `saveSettingURL` variables must be set')
|
||||
}
|
||||
|
||||
if (document.getElementsByClassName('.depends-on-icon').length > 0) {
|
||||
new bootstrap.Tooltip('.depends-on-icon', {
|
||||
placement: 'right',
|
||||
})
|
||||
}
|
||||
$('select.custom-select[multiple]').select2()
|
||||
|
||||
$('.settings-tabs a[data-bs-toggle="tab"]').on('shown.bs.tab', function (event) {
|
||||
$('[data-bs-spy="scroll"]').trigger('scroll.bs.scrollspy')
|
||||
})
|
||||
|
||||
$('.tab-content input, .tab-content select').on('input', function () {
|
||||
if ($(this).attr('type') == 'checkbox') {
|
||||
const $input = $(this)
|
||||
const $inputGroup = $(this).closest('.setting-group')
|
||||
const settingName = $(this).data('setting-name')
|
||||
const settingValue = $(this).is(':checked') ? 1 : 0
|
||||
saveSetting($inputGroup[0], $input, settingName, settingValue)
|
||||
} else {
|
||||
handleSettingValueChange($(this))
|
||||
}
|
||||
})
|
||||
|
||||
$('.tab-content .setting-group .btn-save-setting').click(function () {
|
||||
const $input = $(this).closest('.input-group').find('input, select')
|
||||
const settingName = $input.data('setting-name')
|
||||
const settingValue = $input.val()
|
||||
saveSetting(this, $input, settingName, settingValue)
|
||||
})
|
||||
$('.tab-content .setting-group .btn-reset-setting').click(function () {
|
||||
const $btn = $(this)
|
||||
const $input = $btn.closest('.input-group').find('input, select')
|
||||
let oldValue = window.settingsFlattened[$input.data('setting-name')].value
|
||||
if ($input.is('select')) {
|
||||
oldValue = oldValue !== undefined ? oldValue : -1
|
||||
} else {
|
||||
oldValue = oldValue !== undefined ? oldValue : ''
|
||||
}
|
||||
$input.val(oldValue)
|
||||
handleSettingValueChange($input)
|
||||
})
|
||||
|
||||
const referencedID = window.location.hash
|
||||
redirectToSetting(referencedID)
|
||||
})
|
||||
|
||||
function saveSetting(statusNode, $input, settingName, settingValue) {
|
||||
saveUserSetting(statusNode, settingName, settingValue).then((result) => {
|
||||
window.settingsFlattened[settingName] = result.data
|
||||
if ($input.attr('type') == 'checkbox') {
|
||||
$input.prop('checked', result.data.value == true)
|
||||
} else {
|
||||
$input.val(result.data.value)
|
||||
}
|
||||
handleSettingValueChange($input)
|
||||
}).catch((e) => { })
|
||||
}
|
||||
|
||||
function handleSettingValueChange($input) {
|
||||
let oldValue = window.settingsFlattened[$input.data('setting-name')].value
|
||||
const newValue = ($input.attr('type') == 'checkbox' ? $input.is(':checked') : $input.val())
|
||||
if ($input.attr('type') == 'checkbox') {
|
||||
oldValue = oldValue == true
|
||||
}
|
||||
if (newValue == oldValue || (newValue == '' && oldValue == undefined)) {
|
||||
restoreWarnings($input)
|
||||
} else {
|
||||
removeWarnings($input)
|
||||
}
|
||||
}
|
||||
|
||||
function removeWarnings($input) {
|
||||
const $inputGroup = $input.closest('.input-group')
|
||||
const $btnSettingAction = $inputGroup.find('.btn-setting-action')
|
||||
const $saveButton = $('.setting-group button.btn-save-setting')
|
||||
$input.removeClass(['is-invalid', 'border-warning', 'border-danger', 'border-info', 'warning', 'info'])
|
||||
$btnSettingAction.removeClass('d-none')
|
||||
if ($input.is('select') && $input.find('option:selected').data('is-empty-option') == 1) {
|
||||
$btnSettingAction.addClass('d-none') // hide save button if empty selection picked
|
||||
}
|
||||
$inputGroup.parent().find('.invalid-feedback').removeClass('d-block')
|
||||
}
|
||||
|
||||
function restoreWarnings($input) {
|
||||
const $inputGroup = $input.closest('.input-group')
|
||||
const $btnSettingAction = $inputGroup.find('.btn-setting-action')
|
||||
const $saveButton = $('.setting-group button.btn-save-setting')
|
||||
const setting = window.settingsFlattened[$input.data('setting-name')]
|
||||
if (setting.error) {
|
||||
borderVariant = setting.severity !== undefined ? variantFromSeverity[setting.severity] : 'warning'
|
||||
$input.addClass(['is-invalid', `border-${borderVariant}`, borderVariant])
|
||||
$inputGroup.parent().find('.invalid-feedback').addClass('d-block').text(setting.errorMessage)
|
||||
} else {
|
||||
removeWarnings($input)
|
||||
}
|
||||
const $callout = $input.closest('.settings-group')
|
||||
updateCalloutColors($callout)
|
||||
$btnSettingAction.addClass('d-none')
|
||||
}
|
||||
|
||||
function updateCalloutColors($callout) {
|
||||
if ($callout.length == 0) {
|
||||
return
|
||||
}
|
||||
const $settings = $callout.find('input, select')
|
||||
const settingNames = Array.from($settings).map((i) => {
|
||||
return $(i).data('setting-name')
|
||||
})
|
||||
const severityMapping = { null: 0, info: 1, warning: 2, critical: 3 }
|
||||
const severityMappingInverted = Object.assign({}, ...Object.entries(severityMapping).map(([k, v]) => ({ [v]: k })))
|
||||
let highestSeverity = severityMapping[null]
|
||||
settingNames.forEach(name => {
|
||||
if (window.settingsFlattened[name].error) {
|
||||
highestSeverity = severityMapping[window.settingsFlattened[name].severity] > highestSeverity ? severityMapping[window.settingsFlattened[name].severity] : highestSeverity
|
||||
}
|
||||
});
|
||||
highestSeverity = severityMappingInverted[highestSeverity]
|
||||
$callout.removeClass(['callout', 'callout-danger', 'callout-warning', 'callout-info'])
|
||||
if (highestSeverity !== null) {
|
||||
$callout.addClass(['callout', `callout-${variantFromSeverity[highestSeverity]}`])
|
||||
}
|
||||
}
|
||||
|
||||
function redirectToSetting(referencedID) {
|
||||
const $settingToFocus = $(referencedID)
|
||||
const pageNavID = $(referencedID).closest('.tab-pane').attr('aria-labelledby')
|
||||
const $navController = $(`#${pageNavID}`)
|
||||
const $settingGroup = $settingToFocus.closest('.settings-group')
|
||||
$navController
|
||||
.on('shown.bs.tab.after-redirect', () => {
|
||||
$settingToFocus[0].scrollIntoView()
|
||||
const inputID = $settingToFocus.parent().attr('for')
|
||||
$settingToFocus.closest('.setting-group').find(`#${inputID}`).focus()
|
||||
$navController.off('shown.bs.tab.after-redirect')
|
||||
$settingGroup.addClass(['to-be-slided', 'slide-in'])
|
||||
})
|
||||
.tab('show')
|
||||
$settingGroup.on('webkitAnimationEnd oanimationend msAnimationEnd animationend', function () {
|
||||
$(this).removeClass(['to-be-slided', 'slide-in'])
|
||||
});
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// function saveHiddenColumns(table_setting_id, newTableSettings) {
|
||||
function mergeAndSaveSettings(table_setting_id, newTableSettings) {
|
||||
const settingName = 'ui.table_setting'
|
||||
const urlGet = `/user-settings/getSettingByName/${settingName}`
|
||||
AJAXApi.quickFetchJSON(urlGet).then(tableSettings => {
|
||||
tableSettings = JSON.parse(tableSettings.value)
|
||||
newTableSettings = mergeNewTableSettingsIntoOld(table_setting_id, tableSettings, newTableSettings)
|
||||
saveTableSetting(settingName, newTableSettings)
|
||||
}).catch((e) => { // setting probably doesn't exist
|
||||
saveTableSetting(settingName, newTableSettings)
|
||||
})
|
||||
}
|
||||
|
||||
function mergeNewTableSettingsIntoOld(table_setting_id, oldTableSettings, newTableSettings) {
|
||||
// Merge recursively
|
||||
tableSettings = Object.assign({}, oldTableSettings, newTableSettings)
|
||||
tableSettings[table_setting_id] = Object.assign({}, oldTableSettings[table_setting_id], newTableSettings[table_setting_id])
|
||||
return tableSettings
|
||||
}
|
||||
|
||||
function saveTableSetting(settingName, newTableSettings) {
|
||||
const urlSet = `/user-settings/setSetting/${settingName}`
|
||||
AJAXApi.quickFetchAndPostForm(urlSet, {
|
||||
value: JSON.stringify(newTableSettings)
|
||||
}, {
|
||||
provideFeedback: false
|
||||
}).then(() => {
|
||||
UI.toast({
|
||||
variant: 'success',
|
||||
title: 'Table setting saved',
|
||||
delay: 3000
|
||||
})
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue