Merge branch 'develop' of github.com:cerebrate-project/cerebrate into develop
commit
411a37bfbf
|
@ -1,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
use Cake\ORM\TableRegistry;
|
use Cake\ORM\TableRegistry;
|
||||||
|
use Authentication\PasswordHasher\DefaultPasswordHasher;
|
||||||
|
|
||||||
require_once(ROOT . DS . 'libraries' . DS . 'default' . DS . 'InboxProcessors' . DS . 'GenericInboxProcessor.php');
|
require_once(ROOT . DS . 'libraries' . DS . 'default' . DS . 'InboxProcessors' . DS . 'GenericInboxProcessor.php');
|
||||||
|
|
||||||
|
@ -42,13 +43,25 @@ class RegistrationProcessor extends UserInboxProcessor implements GenericInboxPr
|
||||||
'message' => 'E-mail must be valid'
|
'message' => 'E-mail must be valid'
|
||||||
])
|
])
|
||||||
->notEmpty('first_name', 'A first name must be provided')
|
->notEmpty('first_name', 'A first name must be provided')
|
||||||
->notEmpty('last_name', 'A last name must be provided');
|
->notEmpty('last_name', 'A last name must be provided')
|
||||||
|
->add('password', 'password_complexity', [
|
||||||
|
'rule' => function($value, $context) {
|
||||||
|
if (!preg_match('/^((?=.*\d)|(?=.*\W+))(?![\n])(?=.*[A-Z])(?=.*[a-z]).*$|.{16,}/s', $value) || strlen($value) < 12) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'message' => __('Invalid password. Passwords have to be either 16 character long or 12 character long with 3/4 special groups.')
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create($requestData) {
|
public function create($requestData) {
|
||||||
$this->validateRequestData($requestData);
|
$this->validateRequestData($requestData);
|
||||||
|
$requestData['data']['password'] = (new DefaultPasswordHasher())->hash($requestData['data']['password']);
|
||||||
$requestData['title'] = __('User account creation requested for {0}', $requestData['data']['email']);
|
$requestData['title'] = __('User account creation requested for {0}', $requestData['data']['email']);
|
||||||
return parent::create($requestData);
|
$creationResponse = parent::create($requestData);
|
||||||
|
$creationResponse['message'] = __('User account creation requested. Please wait for an admin to approve your account.');
|
||||||
|
return $creationResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getViewVariables($request)
|
public function getViewVariables($request)
|
||||||
|
@ -72,6 +85,11 @@ class RegistrationProcessor extends UserInboxProcessor implements GenericInboxPr
|
||||||
'username' => !empty($request['data']['username']) ? $request['data']['username'] : '',
|
'username' => !empty($request['data']['username']) ? $request['data']['username'] : '',
|
||||||
'role_id' => !empty($request['data']['role_id']) ? $request['data']['role_id'] : '',
|
'role_id' => !empty($request['data']['role_id']) ? $request['data']['role_id'] : '',
|
||||||
'disabled' => !empty($request['data']['disabled']) ? $request['data']['disabled'] : '',
|
'disabled' => !empty($request['data']['disabled']) ? $request['data']['disabled'] : '',
|
||||||
|
|
||||||
|
'email' => !empty($request['data']['email']) ? $request['data']['email'] : '',
|
||||||
|
'first_name' => !empty($request['data']['first_name']) ? $request['data']['first_name'] : '',
|
||||||
|
'last_name' => !empty($request['data']['last_name']) ? $request['data']['last_name'] : '',
|
||||||
|
'position' => !empty($request['data']['position']) ? $request['data']['position'] : '',
|
||||||
]);
|
]);
|
||||||
return [
|
return [
|
||||||
'dropdownData' => $dropdownData,
|
'dropdownData' => $dropdownData,
|
||||||
|
@ -82,6 +100,7 @@ class RegistrationProcessor extends UserInboxProcessor implements GenericInboxPr
|
||||||
|
|
||||||
public function process($id, $requestData, $inboxRequest)
|
public function process($id, $requestData, $inboxRequest)
|
||||||
{
|
{
|
||||||
|
$hashedPassword = $inboxRequest['data']['password'];
|
||||||
if ($requestData['individual_id'] == -1) {
|
if ($requestData['individual_id'] == -1) {
|
||||||
$individual = $this->Users->Individuals->newEntity([
|
$individual = $this->Users->Individuals->newEntity([
|
||||||
'uuid' => $requestData['uuid'],
|
'uuid' => $requestData['uuid'],
|
||||||
|
@ -101,6 +120,7 @@ class RegistrationProcessor extends UserInboxProcessor implements GenericInboxPr
|
||||||
'role_id' => $requestData['role_id'],
|
'role_id' => $requestData['role_id'],
|
||||||
'disabled' => $requestData['disabled'],
|
'disabled' => $requestData['disabled'],
|
||||||
]);
|
]);
|
||||||
|
$user->set('password', $hashedPassword, ['setter' => false]); // ignore default password hashing as it has already been hashed
|
||||||
$user = $this->Users->save($user);
|
$user = $this->Users->save($user);
|
||||||
|
|
||||||
if ($user !== false) {
|
if ($user !== false) {
|
||||||
|
|
|
@ -1,99 +1,88 @@
|
||||||
<?php
|
<?php
|
||||||
$formUser = $this->element('genericElements/Form/genericForm', [
|
$combinedForm = $this->element('genericElements/Form/genericForm', [
|
||||||
'entity' => $userEntity,
|
'entity' => $userEntity,
|
||||||
'ajax' => false,
|
'ajax' => false,
|
||||||
'raw' => true,
|
'raw' => true,
|
||||||
'data' => [
|
'data' => [
|
||||||
'description' => __('Create user account'),
|
'description' => __('Create user account'),
|
||||||
'model' => 'User',
|
'model' => 'User',
|
||||||
'fields' => [
|
'fields' => [
|
||||||
[
|
[
|
||||||
'field' => 'individual_id',
|
'field' => 'individual_id',
|
||||||
'type' => 'dropdown',
|
'type' => 'dropdown',
|
||||||
'label' => __('Associated individual'),
|
'label' => __('Associated individual'),
|
||||||
'options' => $dropdownData['individual'],
|
'options' => $dropdownData['individual'],
|
||||||
],
|
|
||||||
[
|
|
||||||
'field' => 'username',
|
|
||||||
'autocomplete' => 'off',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'field' => 'role_id',
|
|
||||||
'type' => 'dropdown',
|
|
||||||
'label' => __('Role'),
|
|
||||||
'options' => $dropdownData['role']
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'field' => 'disabled',
|
|
||||||
'type' => 'checkbox',
|
|
||||||
'label' => 'Disable'
|
|
||||||
]
|
|
||||||
],
|
],
|
||||||
'submit' => [
|
[
|
||||||
'action' => $this->request->getParam('action')
|
'field' => 'username',
|
||||||
]
|
'autocomplete' => 'off',
|
||||||
]
|
],
|
||||||
]);
|
[
|
||||||
|
'field' => 'role_id',
|
||||||
$formIndividual = $this->element('genericElements/Form/genericForm', [
|
'type' => 'dropdown',
|
||||||
'entity' => $individualEntity,
|
'label' => __('Role'),
|
||||||
'ajax' => false,
|
'options' => $dropdownData['role']
|
||||||
'raw' => true,
|
],
|
||||||
'data' => [
|
[
|
||||||
'description' => __('Create individual'),
|
'field' => 'disabled',
|
||||||
'model' => 'Individual',
|
'type' => 'checkbox',
|
||||||
'fields' => [
|
'label' => 'Disable'
|
||||||
[
|
|
||||||
'field' => 'email',
|
|
||||||
'autocomplete' => 'off'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'field' => 'uuid',
|
|
||||||
'label' => 'UUID',
|
|
||||||
'type' => 'uuid',
|
|
||||||
'autocomplete' => 'off'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'field' => 'first_name',
|
|
||||||
'autocomplete' => 'off'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'field' => 'last_name',
|
|
||||||
'autocomplete' => 'off'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'field' => 'position',
|
|
||||||
'autocomplete' => 'off'
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
'submit' => [
|
|
||||||
'action' => $this->request->getParam('action')
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
|
|
||||||
echo $this->Bootstrap->modal([
|
sprintf('<div class="pb-2 fs-4">%s</div>', __('Create individual')),
|
||||||
'title' => __('Register user'),
|
[
|
||||||
'size' => 'lg',
|
'field' => 'email',
|
||||||
'type' => 'confirm',
|
'autocomplete' => 'off'
|
||||||
'bodyHtml' => sprintf('<div class="user-container">%s</div><div class="individual-container">%s</div>',
|
],
|
||||||
$formUser,
|
[
|
||||||
$formIndividual
|
'field' => 'uuid',
|
||||||
),
|
'label' => 'UUID',
|
||||||
'confirmText' => __('Create user'),
|
'type' => 'uuid',
|
||||||
'confirmFunction' => 'submitRegistration'
|
'autocomplete' => 'off'
|
||||||
]);
|
],
|
||||||
|
[
|
||||||
|
'field' => 'first_name',
|
||||||
|
'autocomplete' => 'off'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'field' => 'last_name',
|
||||||
|
'autocomplete' => 'off'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'field' => 'position',
|
||||||
|
'autocomplete' => 'off'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'submit' => [
|
||||||
|
'action' => $this->request->getParam('action')
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
echo $this->Bootstrap->modal([
|
||||||
|
'title' => __('Register user'),
|
||||||
|
'size' => 'lg',
|
||||||
|
'type' => 'confirm',
|
||||||
|
'bodyHtml' => sprintf(
|
||||||
|
'<div class="form-container">%s</div>',
|
||||||
|
$combinedForm
|
||||||
|
),
|
||||||
|
'confirmText' => __('Create user'),
|
||||||
|
'confirmFunction' => 'submitRegistration'
|
||||||
|
]);
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function submitRegistration(modalObject, tmpApi) {
|
function submitRegistration(modalObject, tmpApi) {
|
||||||
const $forms = modalObject.$modal.find('form')
|
const $form = modalObject.$modal.find('form')
|
||||||
const url = $forms[0].action
|
tmpApi.postForm($form[0]).then(() => {
|
||||||
const data1 = getFormData($forms[0])
|
const url = '/inbox/index'
|
||||||
const data2 = getFormData($forms[1])
|
const $container = $('div[id^="table-container-"]')
|
||||||
const data = {...data1, ...data2}
|
const randomValue = $container.attr('id').split('-')[2]
|
||||||
return tmpApi.postData(url, data)
|
UI.reload(url, $(`#table-container-${randomValue}`), $(`#table-container-${randomValue} table.table`))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
@ -107,7 +96,7 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
function getFormData(form) {
|
function getFormData(form) {
|
||||||
return Object.values(form).reduce((obj,field) => {
|
return Object.values(form).reduce((obj, field) => {
|
||||||
if (field.type === 'checkbox') {
|
if (field.type === 'checkbox') {
|
||||||
obj[field.name] = field.checked;
|
obj[field.name] = field.checked;
|
||||||
} else {
|
} else {
|
||||||
|
@ -119,7 +108,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div.individual-container > div, div.user-container > div {
|
div.individual-container>div,
|
||||||
font-size: 1.5rem;
|
div.user-container>div {
|
||||||
}
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -119,8 +119,10 @@ function initSelect2Picker($select) {
|
||||||
}
|
}
|
||||||
return buildTag(state)
|
return buildTag(state)
|
||||||
}
|
}
|
||||||
|
const $modal = $select.closest('.modal')
|
||||||
|
|
||||||
$select.select2({
|
$select.select2({
|
||||||
|
dropdownParent: $modal.length != 0 ? $modal.find('.modal-body') : $(document.body),
|
||||||
placeholder: 'Pick a tag',
|
placeholder: 'Pick a tag',
|
||||||
tags: true,
|
tags: true,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
|
@ -254,7 +254,7 @@ class ACLComponent extends Component
|
||||||
*/
|
*/
|
||||||
public function setPublicInterfaces(): void
|
public function setPublicInterfaces(): void
|
||||||
{
|
{
|
||||||
$this->Authentication->allowUnauthenticated(['login']);
|
$this->Authentication->allowUnauthenticated(['login', 'register']);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkAccessInternal($controller, $action, $soft): bool
|
private function checkAccessInternal($controller, $action, $soft): bool
|
||||||
|
|
|
@ -90,7 +90,7 @@ class CRUDComponent extends Component
|
||||||
$this->Controller->set('taggingEnabled', true);
|
$this->Controller->set('taggingEnabled', true);
|
||||||
$this->setAllTags();
|
$this->setAllTags();
|
||||||
}
|
}
|
||||||
$filters = !empty($this->Controller->filters) ? $this->Controller->filters : [];
|
$filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : [];
|
||||||
$this->Controller->set('filters', $filters);
|
$this->Controller->set('filters', $filters);
|
||||||
$this->Controller->viewBuilder()->setLayout('ajax');
|
$this->Controller->viewBuilder()->setLayout('ajax');
|
||||||
$this->Controller->render('/genericTemplates/filters');
|
$this->Controller->render('/genericTemplates/filters');
|
||||||
|
|
|
@ -63,6 +63,11 @@ class Sidemenu {
|
||||||
'icon' => $this->iconTable['UserSettings'],
|
'icon' => $this->iconTable['UserSettings'],
|
||||||
'url' => '/user-settings/index',
|
'url' => '/user-settings/index',
|
||||||
],
|
],
|
||||||
|
'LocalTools.index' => [
|
||||||
|
'label' => __('Local Tools'),
|
||||||
|
'icon' => $this->iconTable['LocalTools'],
|
||||||
|
'url' => '/localTools/index',
|
||||||
|
],
|
||||||
'Messages' => [
|
'Messages' => [
|
||||||
'label' => __('Messages'),
|
'label' => __('Messages'),
|
||||||
'icon' => $this->iconTable['Inbox'],
|
'icon' => $this->iconTable['Inbox'],
|
||||||
|
@ -87,11 +92,6 @@ class Sidemenu {
|
||||||
'icon' => $this->iconTable['MetaTemplates'],
|
'icon' => $this->iconTable['MetaTemplates'],
|
||||||
'url' => '/metaTemplates/index',
|
'url' => '/metaTemplates/index',
|
||||||
],
|
],
|
||||||
'LocalTools.index' => [
|
|
||||||
'label' => __('Local Tools'),
|
|
||||||
'icon' => $this->iconTable['LocalTools'],
|
|
||||||
'url' => '/localTools/index',
|
|
||||||
],
|
|
||||||
'Tags.index' => [
|
'Tags.index' => [
|
||||||
'label' => __('Tags'),
|
'label' => __('Tags'),
|
||||||
'icon' => $this->iconTable['Tags'],
|
'icon' => $this->iconTable['Tags'],
|
||||||
|
|
|
@ -6,6 +6,8 @@ use Cake\Utility\Hash;
|
||||||
use Cake\Utility\Text;
|
use Cake\Utility\Text;
|
||||||
use Cake\ORM\TableRegistry;
|
use Cake\ORM\TableRegistry;
|
||||||
use \Cake\Database\Expression\QueryExpression;
|
use \Cake\Database\Expression\QueryExpression;
|
||||||
|
use Cake\Http\Exception\UnauthorizedException;
|
||||||
|
use Cake\Core\Configure;
|
||||||
|
|
||||||
class UsersController extends AppController
|
class UsersController extends AppController
|
||||||
{
|
{
|
||||||
|
@ -160,19 +162,27 @@ class UsersController extends AppController
|
||||||
|
|
||||||
public function register()
|
public function register()
|
||||||
{
|
{
|
||||||
$this->InboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors');
|
if (empty(Configure::read('Cerebrate')['security.registration.self-registration'])) {
|
||||||
$processor = $this->InboxProcessors->getProcessor('User', 'Registration');
|
throw new UnauthorizedException(__('User self-registration is not open.'));
|
||||||
$data = [
|
}
|
||||||
'origin' => '127.0.0.1',
|
if ($this->request->is('post')) {
|
||||||
'comment' => 'Hi there!, please create an account',
|
$data = $this->request->getData();
|
||||||
'data' => [
|
$this->InboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors');
|
||||||
'username' => 'foobar',
|
$processor = $this->InboxProcessors->getProcessor('User', 'Registration');
|
||||||
'email' => 'foobar@admin.test',
|
$data = [
|
||||||
'first_name' => 'foo',
|
'origin' => $this->request->clientIp(),
|
||||||
'last_name' => 'bar',
|
'comment' => '-no comment-',
|
||||||
],
|
'data' => [
|
||||||
];
|
'username' => $data['username'],
|
||||||
$processorResult = $processor->create($data);
|
'email' => $data['email'],
|
||||||
return $processor->genHTTPReply($this, $processorResult, ['controller' => 'Inbox', 'action' => 'index']);
|
'first_name' => $data['first_name'],
|
||||||
|
'last_name' => $data['last_name'],
|
||||||
|
'password' => $data['password'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$processorResult = $processor->create($data);
|
||||||
|
return $processor->genHTTPReply($this, $processorResult, ['controller' => 'Inbox', 'action' => 'index']);
|
||||||
|
}
|
||||||
|
$this->viewBuilder()->setLayout('login');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -263,6 +263,16 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
'Security' => [
|
'Security' => [
|
||||||
|
'Registration' => [
|
||||||
|
'Registration' => [
|
||||||
|
'security.registration.self-registration' => [
|
||||||
|
'name' => __('Allow self-registration'),
|
||||||
|
'type' => 'boolean',
|
||||||
|
'description' => __('Enable the self-registration feature where user can request account creation. Admin can view the request and accept it in the application inbox.'),
|
||||||
|
'default' => false,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
],
|
||||||
'Development' => [
|
'Development' => [
|
||||||
'Debugging' => [
|
'Debugging' => [
|
||||||
'debug' => [
|
'debug' => [
|
||||||
|
|
|
@ -1,53 +1,58 @@
|
||||||
<?php
|
<?php
|
||||||
use Cake\Core\Configure;
|
use Cake\Core\Configure;
|
||||||
|
|
||||||
echo '<div class="form-signin panel shadow position-absolute start-50 translate-middle">';
|
|
||||||
echo sprintf(
|
|
||||||
'<div class="text-center mb-4">%s</div>',
|
|
||||||
$this->Html->image('logo-purple.png', [
|
|
||||||
'alt' => __('Cerebrate logo'),
|
|
||||||
'width' => 100, 'height' => 100,
|
|
||||||
'style' => ['filter: drop-shadow(4px 4px 4px #924da666);']
|
|
||||||
])
|
|
||||||
);
|
|
||||||
echo sprintf('<h4 class="text-uppercase fw-light mb-3">%s</h4>', __('Sign in'));
|
|
||||||
$template = [
|
|
||||||
'inputContainer' => '<div class="form-floating input {{type}}{{required}}">{{content}}</div>',
|
|
||||||
'formGroup' => '{{input}}{{label}}',
|
|
||||||
'submitContainer' => '<div class="submit d-grid">{{content}}</div>',
|
|
||||||
];
|
|
||||||
$this->Form->setTemplates($template);
|
|
||||||
echo $this->Form->create(null, ['url' => ['controller' => 'users', 'action' => 'login']]);
|
|
||||||
echo $this->Form->control('username', ['label' => 'Username', 'class' => 'form-control mb-2', 'placeholder' => __('Username')]);
|
|
||||||
echo $this->Form->control('password', ['type' => 'password', 'label' => 'Password', 'class' => 'form-control mb-3', 'placeholder' => __('Password')]);
|
|
||||||
echo $this->Form->control(__('Submit'), ['type' => 'submit', 'class' => 'btn btn-primary']);
|
|
||||||
echo $this->Form->end();
|
|
||||||
|
|
||||||
if (!empty(Configure::read('keycloak'))) {
|
|
||||||
echo sprintf('<div class="d-flex align-items-center my-3"><hr class="d-inline-block flex-grow-1"/><span class="mx-3 fw-light">%s</span><hr class="d-inline-block flex-grow-1"/></div>', __('Or'));
|
|
||||||
echo $this->Form->create(null, [
|
|
||||||
'url' => Cake\Routing\Router::url([
|
|
||||||
'prefix' => false,
|
|
||||||
'plugin' => 'ADmad/SocialAuth',
|
|
||||||
'controller' => 'Auth',
|
|
||||||
'action' => 'login',
|
|
||||||
'provider' => 'keycloak',
|
|
||||||
'?' => ['redirect' => $this->request->getQuery('redirect')]
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
echo $this->Bootstrap->button([
|
|
||||||
'type' => 'submit',
|
|
||||||
'text' => __('Login with Keycloak'),
|
|
||||||
'variant' => 'secondary',
|
|
||||||
'class' => ['d-block', 'w-100'],
|
|
||||||
'image' => [
|
|
||||||
'path' => '/img/keycloak_logo.png',
|
|
||||||
'alt' => 'Keycloak'
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
echo $this->Form->end();
|
|
||||||
}
|
|
||||||
echo '</div>';
|
|
||||||
|
|
||||||
?>
|
?>
|
||||||
</div>
|
|
||||||
|
<div class="form-signin panel shadow position-absolute start-50 translate-middle">
|
||||||
|
<?php
|
||||||
|
echo sprintf(
|
||||||
|
'<div class="text-center mb-4">%s</div>',
|
||||||
|
$this->Html->image('logo-purple.png', [
|
||||||
|
'alt' => __('Cerebrate logo'),
|
||||||
|
'width' => 100, 'height' => 100,
|
||||||
|
'style' => ['filter: drop-shadow(4px 4px 4px #924da666);']
|
||||||
|
])
|
||||||
|
);
|
||||||
|
echo sprintf('<h4 class="text-uppercase fw-light mb-3">%s</h4>', __('Sign In'));
|
||||||
|
$template = [
|
||||||
|
'inputContainer' => '<div class="form-floating input {{type}}{{required}}">{{content}}</div>',
|
||||||
|
'formGroup' => '{{input}}{{label}}',
|
||||||
|
'submitContainer' => '<div class="submit d-grid">{{content}}</div>',
|
||||||
|
];
|
||||||
|
$this->Form->setTemplates($template);
|
||||||
|
echo $this->Form->create(null, ['url' => ['controller' => 'users', 'action' => 'login']]);
|
||||||
|
echo $this->Form->control('username', ['label' => 'Username', 'class' => 'form-control mb-2', 'placeholder' => __('Username')]);
|
||||||
|
echo $this->Form->control('password', ['type' => 'password', 'label' => 'Password', 'class' => 'form-control mb-3', 'placeholder' => __('Password')]);
|
||||||
|
echo $this->Form->control(__('Login'), ['type' => 'submit', 'class' => 'btn btn-primary']);
|
||||||
|
echo $this->Form->end();
|
||||||
|
if (!empty(Configure::read('Cerebrate')['security.registration.self-registration'])) {
|
||||||
|
echo '<div class="text-end">';
|
||||||
|
echo sprintf('<span class="text-secondary ms-auto" style="font-size: 0.8rem">%s <a href="/users/register" class="text-decoration-none link-primary fw-bold">%s</a></span>', __('Doesn\'t have an account?'), __('Sign up'));
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty(Configure::read('keycloak'))) {
|
||||||
|
echo sprintf('<div class="d-flex align-items-center my-2"><hr class="d-inline-block flex-grow-1"/><span class="mx-3 fw-light">%s</span><hr class="d-inline-block flex-grow-1"/></div>', __('Or'));
|
||||||
|
echo $this->Form->create(null, [
|
||||||
|
'url' => Cake\Routing\Router::url([
|
||||||
|
'prefix' => false,
|
||||||
|
'plugin' => 'ADmad/SocialAuth',
|
||||||
|
'controller' => 'Auth',
|
||||||
|
'action' => 'login',
|
||||||
|
'provider' => 'keycloak',
|
||||||
|
'?' => ['redirect' => $this->request->getQuery('redirect')]
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
echo $this->Bootstrap->button([
|
||||||
|
'type' => 'submit',
|
||||||
|
'text' => __('Login with Keycloak'),
|
||||||
|
'variant' => 'secondary',
|
||||||
|
'class' => ['d-block', 'w-100'],
|
||||||
|
'image' => [
|
||||||
|
'path' => '/img/keycloak_logo.png',
|
||||||
|
'alt' => 'Keycloak'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
echo $this->Form->end();
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
use Cake\Core\Configure;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="form-signin panel shadow position-absolute start-50 translate-middle">
|
||||||
|
<?php
|
||||||
|
echo sprintf(
|
||||||
|
'<div class="text-center mb-4">%s</div>',
|
||||||
|
$this->Html->image('logo-purple.png', [
|
||||||
|
'alt' => __('Cerebrate logo'),
|
||||||
|
'width' => 100, 'height' => 100,
|
||||||
|
'style' => ['filter: drop-shadow(4px 4px 4px #924da666);']
|
||||||
|
])
|
||||||
|
);
|
||||||
|
echo sprintf('<h4 class="text-uppercase fw-light mb-3">%s</h4>', __('Sign up'));
|
||||||
|
$template = [
|
||||||
|
'inputContainer' => '<div class="input {{type}}{{required}}">{{content}}</div>',
|
||||||
|
'formGroup' => '{{label}}{{input}}',
|
||||||
|
'submitContainer' => '<div class="submit d-grid">{{content}}</div>',
|
||||||
|
'label' => '<label class="fw-light fs-7" {{attrs}}>{{text}}</label>'
|
||||||
|
];
|
||||||
|
$this->Form->setTemplates($template);
|
||||||
|
echo $this->Form->create(null, ['url' => ['controller' => 'users', 'action' => 'register']]);
|
||||||
|
|
||||||
|
echo $this->Form->control('username', ['label' => __('Username'), 'class' => 'form-control mb-2']);
|
||||||
|
echo $this->Form->control('email', ['label' => __('E-mail Address'), 'class' => 'form-control mb-3']);
|
||||||
|
|
||||||
|
echo '<div class="row g-1 mb-3">';
|
||||||
|
echo '<div class="col-md">';
|
||||||
|
echo $this->Form->control('first_name', ['label' => __('First Name'), 'class' => 'form-control']);
|
||||||
|
echo '</div>';
|
||||||
|
echo '<div class="col-md">';
|
||||||
|
echo $this->Form->control('last_name', ['label' => __('Last Name'), 'class' => 'form-control mb-2']);
|
||||||
|
echo '</div>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
echo $this->Form->control('password', ['type' => 'password', 'label' => __('Password'), 'class' => 'form-control mb-4']);
|
||||||
|
|
||||||
|
echo $this->Form->control(__('Sign up'), ['type' => 'submit', 'class' => 'btn btn-primary']);
|
||||||
|
echo '<div class="text-end">';
|
||||||
|
echo sprintf('<span class="text-secondary ms-auto" style="font-size: 0.8rem">%s <a href="/users/login" class="text-decoration-none link-primary fw-bold">%s</a></span>', __('Have an account?'), __('Sign in'));
|
||||||
|
echo '</div>';
|
||||||
|
echo $this->Form->end();
|
||||||
|
?>
|
||||||
|
</div>
|
|
@ -21,9 +21,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
if (!empty($header['element']) && $header['element'] === 'selector') {
|
||||||
|
$columnName = 'row-selector';
|
||||||
|
} else {
|
||||||
|
$columnName = h(\Cake\Utility\Inflector::variable(!empty($header['name']) ? $header['name'] : \Cake\Utility\Inflector::humanize($header['data_path'])));
|
||||||
|
}
|
||||||
$headersHtml .= sprintf(
|
$headersHtml .= sprintf(
|
||||||
'<th scope="col" data-columnname="%s">%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']))),
|
$columnName,
|
||||||
$header_data
|
$header_data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,9 @@ $tableSettings['hidden_column'] = $tableSettings['hidden_column'] ?? [];
|
||||||
$availableColumnsHtml = '';
|
$availableColumnsHtml = '';
|
||||||
$availableColumns = [];
|
$availableColumns = [];
|
||||||
foreach ($table_data['fields'] as $field) {
|
foreach ($table_data['fields'] as $field) {
|
||||||
|
if (!empty($field['element']) && $field['element'] === 'selector') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$fieldName = !empty($field['name']) ? $field['name'] : \Cake\Utility\Inflector::humanize($field['data_path']);
|
$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']);
|
$isVisible = !in_array(h(\Cake\Utility\Inflector::variable($fieldName)), $tableSettings['hidden_column']);
|
||||||
$availableColumns[] = $fieldName;
|
$availableColumns[] = $fieldName;
|
||||||
|
@ -62,6 +65,7 @@ echo $availableColumnsHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
addSupportOfNestedDropdown();
|
||||||
const $form = $('form.visible-column-form')
|
const $form = $('form.visible-column-form')
|
||||||
const $checkboxes = $form.find('input').not(':checked')
|
const $checkboxes = $form.find('input').not(':checked')
|
||||||
const $dropdownMenu = $form.closest('.dropdown')
|
const $dropdownMenu = $form.closest('.dropdown')
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Cake\Utility\Inflector;
|
use Cake\Utility\Inflector;
|
||||||
|
|
||||||
|
$tableItems = array_map(function ($fieldName) {
|
||||||
|
return [
|
||||||
|
'fieldname' => $fieldName,
|
||||||
|
];
|
||||||
|
}, $filters);
|
||||||
|
|
||||||
$filteringForm = $this->Bootstrap->table(
|
$filteringForm = $this->Bootstrap->table(
|
||||||
[
|
[
|
||||||
'small' => true,
|
'small' => true,
|
||||||
|
@ -9,19 +16,36 @@ $filteringForm = $this->Bootstrap->table(
|
||||||
'tableClass' => ['indexFilteringTable'],
|
'tableClass' => ['indexFilteringTable'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'fields' => [
|
'fields' => [
|
||||||
__('Field'),
|
[
|
||||||
__('Operator'),
|
'key' => 'fieldname', 'label' => __('Field'), 'formatter' => function ($field, $row) {
|
||||||
[
|
return sprintf('<span class="fieldName" data-fieldname="%s">%s</span>', h($field), h($field));
|
||||||
'labelHtml' => sprintf('%s %s',
|
}
|
||||||
__('Value'),
|
],
|
||||||
sprintf('<sup class="fa fa-info" title="%s"><sup>', __('Supports strict matches and LIKE matches with the `%` character. Example: `%.com`'))
|
[
|
||||||
)
|
'key' => 'operator', 'label' => __('Operator'), 'formatter' => function ($field, $row) {
|
||||||
|
$options = [
|
||||||
|
sprintf('<option value="%s">%s</option>', '=', '='),
|
||||||
|
sprintf('<option value="%s">%s</option>', '!=', '!='),
|
||||||
|
];
|
||||||
|
return sprintf('<select class="fieldOperator form-select form-select-sm">%s</select>', implode('', $options));
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'value',
|
||||||
|
'labelHtml' => sprintf(
|
||||||
|
'%s %s',
|
||||||
|
__('Value'),
|
||||||
|
sprintf('<sup class="fa fa-info" title="%s"><sup>', __('Supports strict matches and LIKE matches with the `%` character. Example: `%.com`'))
|
||||||
|
),
|
||||||
|
'formatter' => function ($field, $row) {
|
||||||
|
return sprintf('<input type="text" class="fieldValue form-control form-control-sm">');
|
||||||
|
}
|
||||||
|
],
|
||||||
],
|
],
|
||||||
__('Action')
|
'items' => $tableItems
|
||||||
],
|
]
|
||||||
'items' => []
|
);
|
||||||
]);
|
|
||||||
|
|
||||||
if ($taggingEnabled) {
|
if ($taggingEnabled) {
|
||||||
$helpText = $this->Bootstrap->genNode('sup', [
|
$helpText = $this->Bootstrap->genNode('sup', [
|
||||||
|
@ -40,7 +64,6 @@ if ($taggingEnabled) {
|
||||||
}
|
}
|
||||||
$modalBody = sprintf('%s%s', $filteringForm, $filteringTags);
|
$modalBody = sprintf('%s%s', $filteringForm, $filteringTags);
|
||||||
|
|
||||||
|
|
||||||
echo $this->Bootstrap->modal([
|
echo $this->Bootstrap->modal([
|
||||||
'title' => __('Filtering options for {0}', Inflector::singularize($this->request->getParam('controller'))),
|
'title' => __('Filtering options for {0}', Inflector::singularize($this->request->getParam('controller'))),
|
||||||
'size' => 'lg',
|
'size' => 'lg',
|
||||||
|
@ -69,7 +92,9 @@ echo $this->Bootstrap->modal([
|
||||||
if (rowData['operator'] == '!=') {
|
if (rowData['operator'] == '!=') {
|
||||||
fullFilter += ' !='
|
fullFilter += ' !='
|
||||||
}
|
}
|
||||||
activeFilters[fullFilter] = rowData['value']
|
if (rowData['value'].length > 0) {
|
||||||
|
activeFilters[fullFilter] = rowData['value']
|
||||||
|
}
|
||||||
})
|
})
|
||||||
$select = modalObject.$modal.find('select.tag-input')
|
$select = modalObject.$modal.find('select.tag-input')
|
||||||
activeFilters['filteringTags'] = $select.select2('data').map(tag => tag.text)
|
activeFilters['filteringTags'] = $select.select2('data').map(tag => tag.text)
|
||||||
|
@ -85,8 +110,6 @@ echo $this->Bootstrap->modal([
|
||||||
|
|
||||||
function initFilteringTable($filteringTable) {
|
function initFilteringTable($filteringTable) {
|
||||||
const $controlRow = $filteringTable.find('#controlRow')
|
const $controlRow = $filteringTable.find('#controlRow')
|
||||||
$filteringTable.find('tbody').empty()
|
|
||||||
addControlRow($filteringTable)
|
|
||||||
const randomValue = getRandomValue()
|
const randomValue = getRandomValue()
|
||||||
const activeFilters = Object.assign({}, $(`#toggleFilterButton-${randomValue}`).data('activeFilters'))
|
const activeFilters = Object.assign({}, $(`#toggleFilterButton-${randomValue}`).data('activeFilters'))
|
||||||
const tags = activeFilters['filteringTags'] !== undefined ? Object.assign({}, activeFilters)['filteringTags'] : []
|
const tags = activeFilters['filteringTags'] !== undefined ? Object.assign({}, activeFilters)['filteringTags'] : []
|
||||||
|
@ -100,7 +123,7 @@ echo $this->Bootstrap->modal([
|
||||||
} else if (fieldParts.length > 2) {
|
} else if (fieldParts.length > 2) {
|
||||||
console.error('Field contains multiple spaces. ' + field)
|
console.error('Field contains multiple spaces. ' + field)
|
||||||
}
|
}
|
||||||
addFilteringRow($filteringTable, field, value, operator)
|
setFilteringValues($filteringTable, field, value, operator)
|
||||||
}
|
}
|
||||||
$select = $filteringTable.closest('.modal-body').find('select.tag-input')
|
$select = $filteringTable.closest('.modal-body').find('select.tag-input')
|
||||||
let passedTags = []
|
let passedTags = []
|
||||||
|
@ -118,97 +141,17 @@ echo $this->Bootstrap->modal([
|
||||||
.trigger('change')
|
.trigger('change')
|
||||||
}
|
}
|
||||||
|
|
||||||
function addControlRow($filteringTable) {
|
function setFilteringValues($filteringTable, field, value, operator) {
|
||||||
const availableFilters = <?= json_encode($filters) ?>;
|
$row = $filteringTable.find('td > span.fieldName').filter(function() {
|
||||||
const $selectField = $('<select/>').addClass('fieldSelect form-select form-select-sm')
|
return $(this).data('fieldname') == field
|
||||||
availableFilters.forEach(filter => {
|
}).closest('tr')
|
||||||
$selectField.append($('<option/>').text(filter))
|
$row.find('.fieldOperator').val(operator)
|
||||||
});
|
$row.find('.fieldValue').val(value)
|
||||||
const $selectOperator = $('<select/>').addClass('fieldOperator form-select form-select-sm')
|
|
||||||
.append([
|
|
||||||
$('<option/>').text('=').val('='),
|
|
||||||
$('<option/>').text('!=').val('!='),
|
|
||||||
])
|
|
||||||
const $row = $('<tr/>').attr('id', 'controlRow')
|
|
||||||
.append(
|
|
||||||
$('<td/>').append($selectField),
|
|
||||||
$('<td/>').append($selectOperator),
|
|
||||||
$('<td/>').append(
|
|
||||||
$('<input>').attr('type', 'text').addClass('fieldValue form-control form-control-sm')
|
|
||||||
),
|
|
||||||
$('<td/>').append(
|
|
||||||
$('<button/>').attr('type', 'button').addClass('btn btn-sm btn-primary')
|
|
||||||
.append($('<span/>').addClass('fa fa-plus'))
|
|
||||||
.click(addFiltering)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
$filteringTable.append($row)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addFilteringRow($filteringTable, field, value, operator) {
|
|
||||||
const $selectOperator = $('<select/>').addClass('fieldOperator form-select form-select-sm')
|
|
||||||
.append([
|
|
||||||
$('<option/>').text('=').val('='),
|
|
||||||
$('<option/>').text('!=').val('!='),
|
|
||||||
]).val(operator)
|
|
||||||
const $row = $('<tr/>')
|
|
||||||
.append(
|
|
||||||
$('<td/>').text(field).addClass('fieldName').data('fieldName', field),
|
|
||||||
$('<td/>').append($selectOperator),
|
|
||||||
$('<td/>').append(
|
|
||||||
$('<input>').attr('type', 'text').addClass('fieldValue form-control form-control-sm').val(value)
|
|
||||||
),
|
|
||||||
$('<td/>').append(
|
|
||||||
$('<button/>').attr('type', 'button').addClass('btn btn-sm btn-danger')
|
|
||||||
.append($('<span/>').addClass('fa fa-trash'))
|
|
||||||
.click(removeSelf)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
$filteringTable.append($row)
|
|
||||||
const $controlRow = $filteringTable.find('#controlRow')
|
|
||||||
disableOptionFromSelect($controlRow, field)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addFiltering() {
|
|
||||||
const $table = $(this).closest('table.indexFilteringTable')
|
|
||||||
const $controlRow = $table.find('#controlRow')
|
|
||||||
const field = $controlRow.find('select.fieldSelect').val()
|
|
||||||
const value = $controlRow.find('input.fieldValue').val()
|
|
||||||
const operator = $controlRow.find('input.fieldOperator').val()
|
|
||||||
addFilteringRow($table, field, value, operator)
|
|
||||||
$controlRow.find('input.fieldValue').val('')
|
|
||||||
$controlRow.find('select.fieldSelect').val('')
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeSelf() {
|
|
||||||
const $row = $(this).closest('tr')
|
|
||||||
const $controlRow = $row.closest('table.indexFilteringTable').find('#controlRow')
|
|
||||||
const field = $row.data('fieldName')
|
|
||||||
$row.remove()
|
|
||||||
enableOptionFromSelect($controlRow, field)
|
|
||||||
}
|
|
||||||
|
|
||||||
function disableOptionFromSelect($controlRow, optionName) {
|
|
||||||
$controlRow.find('select.fieldSelect option').each(function() {
|
|
||||||
const $option = $(this)
|
|
||||||
if ($option.text() == optionName) {
|
|
||||||
$option.prop('disabled', true)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableOptionFromSelect($controlRow, optionName) {
|
|
||||||
$controlRow.find('select.fieldSelect option').each(function() {
|
|
||||||
const $option = $(this)
|
|
||||||
if ($option.text() == optionName) {
|
|
||||||
$option.prop('disabled', false)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDataFromRow($row) {
|
function getDataFromRow($row) {
|
||||||
const rowData = {};
|
const rowData = {};
|
||||||
rowData['name'] = $row.find('td.fieldName').data('fieldName')
|
rowData['name'] = $row.find('td > span.fieldName').data('fieldname')
|
||||||
rowData['operator'] = $row.find('select.fieldOperator').val()
|
rowData['operator'] = $row.find('select.fieldOperator').val()
|
||||||
rowData['value'] = $row.find('input.fieldValue').val()
|
rowData['value'] = $row.find('input.fieldValue').val()
|
||||||
return rowData
|
return rowData
|
||||||
|
|
|
@ -94,7 +94,7 @@ input[type="checkbox"]:disabled.change-cursor {
|
||||||
}
|
}
|
||||||
|
|
||||||
.select2-container {
|
.select2-container {
|
||||||
z-index: 1056;
|
z-index: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select2-container--bootstrap-5 {
|
.select2-container--bootstrap-5 {
|
||||||
|
@ -167,4 +167,12 @@ input[type="checkbox"]:disabled.change-cursor {
|
||||||
-webkit-mask-size: contain;
|
-webkit-mask-size: contain;
|
||||||
-webkit-mask-repeat: no-repeat;
|
-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");
|
-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");
|
||||||
|
}
|
||||||
|
|
||||||
|
.fs-7 {
|
||||||
|
font-size: .875rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fs-8 {
|
||||||
|
font-size: .7rem !important;
|
||||||
}
|
}
|
|
@ -243,8 +243,13 @@ function overloadBSDropdown() {
|
||||||
return _orginal.call(this);
|
return _orginal.call(this);
|
||||||
}
|
}
|
||||||
}($bs.Dropdown.prototype.toggle);
|
}($bs.Dropdown.prototype.toggle);
|
||||||
|
})(bootstrap);
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelectorAll('.dropdown').forEach(function (dd) {
|
function addSupportOfNestedDropdown() {
|
||||||
|
const CLASS_NAME_HAS_CHILD = 'has-child-dropdown-show';
|
||||||
|
document.querySelectorAll('.dropdown').forEach(function (dd) {
|
||||||
|
if (dd.getAttribute('data-listener-registered') === null) { // Only add listener once
|
||||||
dd.addEventListener('hide.bs.dropdown', function (e) {
|
dd.addEventListener('hide.bs.dropdown', function (e) {
|
||||||
if (this.classList.contains(CLASS_NAME_HAS_CHILD)) {
|
if (this.classList.contains(CLASS_NAME_HAS_CHILD)) {
|
||||||
this.classList.remove(CLASS_NAME_HAS_CHILD);
|
this.classList.remove(CLASS_NAME_HAS_CHILD);
|
||||||
|
@ -261,8 +266,9 @@ function overloadBSDropdown() {
|
||||||
}
|
}
|
||||||
e.stopPropagation(); // do not need pop in multi level mode
|
e.stopPropagation(); // do not need pop in multi level mode
|
||||||
});
|
});
|
||||||
});
|
dd.setAttribute('data-listener-registered', 1)
|
||||||
})(bootstrap);
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var UI
|
var UI
|
||||||
|
@ -271,6 +277,7 @@ $(document).ready(() => {
|
||||||
UI = new UIFactory()
|
UI = new UIFactory()
|
||||||
}
|
}
|
||||||
overloadBSDropdown();
|
overloadBSDropdown();
|
||||||
|
addSupportOfNestedDropdown();
|
||||||
|
|
||||||
const debouncedGlobalSearch = debounce(performGlobalSearch, 400)
|
const debouncedGlobalSearch = debounce(performGlobalSearch, 400)
|
||||||
$('#globalSearch')
|
$('#globalSearch')
|
||||||
|
|
Loading…
Reference in New Issue