Merge branch 'develop' into main

debug-branch
Sami Mokaddem 2023-03-13 15:48:18 +01:00
commit 3b385ade74
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
46 changed files with 1208 additions and 316 deletions

View File

@ -11,7 +11,7 @@ use Cake\Core\Configure;
class AuditLogsController extends AppController
{
public $filterFields = ['model_id', 'model', 'request_action', 'user_id', 'model_title', 'AuditLogs.created'];
public $filterFields = ['model_id', 'model', ['name' => 'request_action', 'multiple' => true, ], 'user_id', 'model_title', 'AuditLogs.created'];
public $quickFilterFields = ['model', 'request_action', 'model_title'];
public $containFields = ['Users'];
@ -20,7 +20,7 @@ class AuditLogsController extends AppController
$this->CRUD->index([
'contain' => $this->containFields,
'order' => ['AuditLogs.id' => 'DESC'],
'filters' => $this->filterFields,
'filters' => $this->CRUD->getFilterFieldsName($this->filterFields),
'quickFilters' => $this->quickFilterFields,
'afterFind' => function($data) {
$request_ip = is_resource($data['request_ip']) ? stream_get_contents($data['request_ip']) : $data['request_ip'];

View File

@ -71,8 +71,12 @@ class AuthKeysController extends AppController
if (empty($currentUser['role']['perm_org_admin'])) {
$userConditions['id'] = $currentUser['id'];
} else {
$role_ids = $this->Users->Roles->find()->where(['perm_admin' => 0])->all()->extract('id')->toList();
$userConditions['role_id IN'] = $role_ids;
$role_ids = $this->Users->Roles->find()->where(['perm_admin' => 0, 'perm_org_admin' => 0])->all()->extract('id')->toList();
$userConditions['organisation_id'] = $currentUser['organisation_id'];
$userConditions['OR'] = [
['role_id IN' => $role_ids],
['id' => $currentUser['id']],
];
}
}
$users = $this->Users->find('list');
@ -99,6 +103,7 @@ class AuthKeysController extends AppController
$dropdownData = [
'user' => $users
];
$this->entity->user_id = $currentUser['id'];
$this->set(compact('dropdownData'));
}
}

View File

@ -90,8 +90,8 @@ class ACLComponent extends Component
'edit' => ['perm_admin', 'perm_org_admin'],
'filtering' => ['*'],
'index' => ['*'],
'tag' => ['perm_tagger'],
'untag' => ['perm_tagger'],
'tag' => ['*'],
'untag' => ['*'],
'view' => ['*'],
'viewTags' => ['*']
],
@ -140,20 +140,24 @@ class ACLComponent extends Component
'enable' => ['perm_admin'],
'getMetaFieldsToUpdate' => ['perm_admin'],
'index' => ['perm_admin'],
'migrateMetafieldsToNewestTemplate' => ['perm_admin'],
'migrateOldMetaTemplateToNewestVersionForEntity' => ['perm_admin'],
'update' => ['perm_admin'],
'updateAllTemplates' => ['perm_admin'],
'toggle' => ['perm_admin'],
'view' => ['perm_admin'],
],
'MetaTemplateNameDirectory' => [
'index' => ['perm_admin'],
],
'Organisations' => [
'add' => ['perm_admin'],
'delete' => ['perm_admin'],
'edit' => ['perm_admin'],
'filtering' => ['*'],
'index' => ['*'],
'tag' => ['perm_tagger'],
'untag' => ['perm_tagger'],
'tag' => ['perm_org_admin'],
'untag' => ['perm_org_admin'],
'view' => ['*'],
'viewTags' => ['*']
],

View File

@ -10,6 +10,7 @@ use Cake\Utility\Text;
use Cake\View\ViewBuilder;
use Cake\ORM\TableRegistry;
use Cake\ORM\Query;
use Cake\Database\Expression\QueryExpression;
use Cake\Routing\Router;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\NotFoundException;
@ -225,22 +226,41 @@ class CRUDComponent extends Component
} else {
$this->Controller->set('metaFieldsEnabled', false);
}
$filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : [];
$filtersConfigRaw= !empty($this->Controller->filterFields) ? $this->Controller->filterFields : [];
$filtersConfig = [];
foreach ($filtersConfigRaw as $fieldConfig) {
if (is_array($fieldConfig)) {
$filtersConfig[$fieldConfig['name']] = $fieldConfig;
} else {
$filtersConfig[$fieldConfig] = ['name' => $fieldConfig];
}
}
$filtersName = $this->getFilterFieldsName();
$typeMap = $this->Table->getSchema()->typeMap();
$associatedtypeMap = !empty($this->Controller->filterFields) ? $this->_getAssociatedTypeMap() : [];
$associatedtypeMap = !empty($filtersName) ? $this->_getAssociatedTypeMap() : [];
$typeMap = array_merge(
$this->Table->getSchema()->typeMap(),
$associatedtypeMap
);
$typeMap = array_filter($typeMap, function ($field) use ($filters) {
return in_array($field, $filters);
$typeMap = array_filter($typeMap, function ($field) use ($filtersName) {
return in_array($field, $filtersName);
}, ARRAY_FILTER_USE_KEY);
$this->Controller->set('typeMap', $typeMap);
$this->Controller->set('filters', $filters);
$this->Controller->set('filters', $filtersName);
$this->Controller->set('filtersConfig', $filtersConfig);
$this->Controller->viewBuilder()->setLayout('ajax');
$this->Controller->render('/genericTemplates/filters');
}
public function getFilterFieldsName(): array
{
$filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : [];
$filters = array_map(function($item) {
return is_array($item) ? $item['name'] : $item;
}, $filters);
return $filters;
}
/**
* getResponsePayload Returns the adaquate response payload based on the request context
*
@ -289,6 +309,9 @@ class CRUDComponent extends Component
if ($this->metaFieldsSupported()) {
$metaTemplates = $this->getMetaTemplates();
$data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
if (isset($params['afterFind'])) {
$data = $params['afterFind']($data, $params);
}
}
if ($this->request->is('post')) {
$patchEntityParams = [
@ -548,16 +571,16 @@ class CRUDComponent extends Component
$query->where($params['conditions']);
}
$data = $query->first();
if ($this->metaFieldsSupported()) {
$metaTemplates = $this->getMetaTemplates();
$data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
}
if (isset($params['afterFind'])) {
$data = $params['afterFind']($data, $params);
}
if (empty($data)) {
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
}
if ($this->metaFieldsSupported()) {
$metaTemplates = $this->getMetaTemplates();
$data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
}
if ($this->request->is(['post', 'put'])) {
$patchEntityParams = [
'associated' => []
@ -828,6 +851,9 @@ class CRUDComponent extends Component
$query->contain($params['contain']);
}
$data = $query->first();
if (isset($params['afterFind'])) {
$data = $params['afterFind']($data, $params);
}
if (empty($data)) {
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
}
@ -850,6 +876,9 @@ class CRUDComponent extends Component
$query->contain($params['contain']);
}
$data = $query->first();
if (isset($params['afterFind'])) {
$data = $params['afterFind']($data, $params);
}
if (isset($params['beforeSave'])) {
try {
$data = $params['beforeSave']($data);
@ -1225,7 +1254,7 @@ class CRUDComponent extends Component
}
$activeFilters[$filter] = $filterValue;
if (is_array($filterValue)) {
$query->where([($filter . ' IN') => $filterValue]);
$query = $this->setInCondition($query, $filter, $filterValue);
} else {
$query = $this->setValueCondition($query, $filter, $filterValue);
}
@ -1311,6 +1340,27 @@ class CRUDComponent extends Component
}
}
protected function setInCondition($query, $fieldName, $values)
{
$split = explode(' ', $fieldName);
if (count($split) == 1) {
$field = $fieldName;
$operator = '=';
} else {
$field = $split[0];
$operator = $split[1];
}
if ($operator == '=') {
return $query->where(function (QueryExpression $exp, Query $q) use ($field, $values) {
return $exp->in($field, $values);
});
} else if ($operator == '!=') {
return $query->where(function (QueryExpression $exp, Query $q) use ($field, $values) {
return $exp->notIn($field, $values);
});
}
}
protected function setFilteringContext($contextFilters, $params)
{
$filteringContexts = [];
@ -1591,7 +1641,7 @@ class CRUDComponent extends Component
protected function _getAssociatedTypeMap(): array
{
$typeMap = [];
foreach ($this->Controller->filterFields as $filter) {
foreach ($this->getFilterFieldsName() as $filter) {
$exploded = explode('.', $filter);
if (count($exploded) > 1) {
$model = $exploded[0];

View File

@ -56,6 +56,12 @@ class MetaTemplatesNavigation extends BaseNavigation
'url' => '/metaTemplates/prune_outdated_template',
'isPOST' => true,
]);
$this->bcf->addRoute('MetaTemplates', 'view_template_directory', [
'label' => __('View all known templates'),
'icon' => 'list',
'url' => '/metaTemplateNameDirectory/index',
'isRedirect' => true,
]);
}
public function addParents()
@ -78,7 +84,7 @@ class MetaTemplatesNavigation extends BaseNavigation
$totalUpdateCount = $udpateCount + $newCount;
}
$updateAllActionConfig = [
'label' => __('Update all template'),
'label' => __('Update all templates'),
'url' => '/metaTemplates/updateAllTemplates',
'url_vars' => ['id' => 'id'],
];
@ -94,6 +100,9 @@ class MetaTemplatesNavigation extends BaseNavigation
'label' => __('Prune outdated templates'),
'url' => '/metaTemplates/prune_outdated_template',
]);
$this->bcf->addAction('MetaTemplates', 'index', 'MetaTemplates', 'view_template_directory', [
'isRedirect' => true,
]);
if (empty($this->viewVars['templateStatus']['up-to-date'])) {
$this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'update', [

View File

@ -15,7 +15,7 @@ use Cake\Error\Debugger;
class EncryptionKeysController extends AppController
{
public $filterFields = ['owner_model', 'owner_id', 'encryption_key'];
public $quickFilterFields = ['encryption_key'];
public $quickFilterFields = [['encryption_key' => true]];
public $containFields = ['Individuals', 'Organisations'];
public $statisticsFields = ['type'];
@ -83,6 +83,9 @@ class EncryptionKeysController extends AppController
$individualConditions = [
'id' => $currentUser['individual_id']
];
$orgConditions = [
'id' => -1, // Only org_admins are allowed to manage their org's encryption keys
];
} else {
$this->loadModel('Alignments');
$individualConditions = ['id IN' => $this->Alignments->find('list', [
@ -122,6 +125,11 @@ class EncryptionKeysController extends AppController
'organisation' => $this->Organisations->find('list')->order(['name' => 'asc'])->where($orgConditions)->all()->toArray(),
'individual' => $this->Individuals->find('list')->order(['email' => 'asc'])->where($individualConditions)->all()->toArray()
];
foreach ($dropdownData as $modelName => $list) {
if (empty($list)) {
unset($dropdownData[$modelName]);
}
}
return $params;
}

View File

@ -68,25 +68,15 @@ class IndividualsController extends AppController
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('canEdit', $this->canEdit($id));
}
public function edit($id)
{
$currentUser = $this->ACL->getUser();
if (!$currentUser['role']['perm_admin']) {
$validIndividuals = $this->Individuals->getValidIndividualsToEdit($currentUser);
if (!in_array($id, $validIndividuals)) {
throw new MethodNotAllowedException(__('You cannot modify that individual.'));
}
if (!$this->canEdit($id)) {
throw new MethodNotAllowedException(__('You cannot modify that individual.'));
}
$currentUser = $this->ACL->getUser();
$validIndividualIds = [];
if (!$currentUser['role']['perm_admin']) {
$validIndividualIds = $this->Individuals->getValidIndividualsToEdit($currentUser);
if (!in_array($id, $validIndividualIds)) {
throw new NotFoundException(__('Invalid individual.'));
}
}
$this->CRUD->edit($id, [
'beforeSave' => function($data) use ($currentUser) {
if ($currentUser['role']['perm_admin'] && isset($data['uuid'])) {
@ -104,7 +94,16 @@ class IndividualsController extends AppController
public function delete($id)
{
$this->CRUD->delete($id);
$params = [
'contain' => ['Users'],
'afterFind' => function($data, $params) {
if (!empty($data['user'])) {
throw new ForbiddenException(__('Individual associated to a user cannot be deleted.'));
}
return $data;
}
];
$this->CRUD->delete($id, $params);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
@ -113,6 +112,9 @@ class IndividualsController extends AppController
public function tag($id)
{
if (!$this->canEdit($id)) {
throw new MethodNotAllowedException(__('You cannot tag that individual.'));
}
$this->CRUD->tag($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@ -122,6 +124,9 @@ class IndividualsController extends AppController
public function untag($id)
{
if (!$this->canEdit($id)) {
throw new MethodNotAllowedException(__('You cannot untag that individual.'));
}
$this->CRUD->untag($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@ -137,4 +142,17 @@ class IndividualsController extends AppController
return $responsePayload;
}
}
private function canEdit($indId): bool
{
$currentUser = $this->ACL->getUser();
if ($currentUser['role']['perm_admin']) {
return true;
}
$validIndividuals = $this->Individuals->getValidIndividualsToEdit($currentUser);
if (in_array($indId, $validIndividuals)) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use Cake\Utility\Inflector;
use Cake\ORM\TableRegistry;
use \Cake\Database\Expression\QueryExpression;
use Cake\Http\Exception\NotFoundException;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Routing\Router;
class MetaTemplateNameDirectoryController extends AppController
{
public $quickFilterFields = [['name' => true], 'uuid', 'version'];
public $filterFields = ['name', 'uuid', 'version'];
public $containFields = ['MetaTemplates'];
public function index()
{
$this->CRUD->index([
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contain' => $this->containFields,
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
}

View File

@ -404,8 +404,18 @@ class MetaTemplatesController extends AppController
$metaTemplate = $this->MetaTemplates->get($template_id, [
'contain' => ['MetaTemplateFields']
]);
$templateOnDisk = $this->MetaTemplates->readTemplateFromDisk($metaTemplate->uuid);
$error = '';
$errorMessage = '';
$templateOnDisk = $this->MetaTemplates->readTemplateFromDisk($metaTemplate->uuid, $error);
if (is_null($templateOnDisk)) {
$errorMessage = __('Could not retreive template\'s status. Reason: {0}', $error);
$this->Flash->error($errorMessage);
$templateOnDisk = [];
}
$templateStatus = $this->MetaTemplates->getStatusForMetaTemplate($templateOnDisk, $metaTemplate);
if (!empty($errorMessage)) {
$templateStatus['error'] = $errorMessage;
}
$this->set('templateOnDisk', $templateOnDisk);
$this->set('templateStatus', $templateStatus);
return [

View File

@ -101,7 +101,6 @@ class OrganisationsController extends AppController
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('metaGroup', 'ContactDB');
}
public function view($id)
@ -111,16 +110,12 @@ class OrganisationsController extends AppController
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('metaGroup', 'ContactDB');
$this->set('canEdit', $this->canEdit($id));
}
public function edit($id)
{
$currentUser = $this->ACL->getUser();
if (
!($currentUser['organisation']['id'] == $id && $currentUser['role']['perm_org_admin']) &&
!$currentUser['role']['perm_admin']
) {
if (!$this->canEdit($id)) {
throw new MethodNotAllowedException(__('You cannot modify that organisation.'));
}
$this->CRUD->edit($id);
@ -144,6 +139,9 @@ class OrganisationsController extends AppController
public function tag($id)
{
if (!$this->canEdit($id)) {
throw new MethodNotAllowedException(__('You cannot tag that organisation.'));
}
$this->CRUD->tag($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@ -153,6 +151,9 @@ class OrganisationsController extends AppController
public function untag($id)
{
if (!$this->canEdit($id)) {
throw new MethodNotAllowedException(__('You cannot untag that organisation.'));
}
$this->CRUD->untag($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@ -168,4 +169,16 @@ class OrganisationsController extends AppController
return $responsePayload;
}
}
private function canEdit($orgId): bool
{
$currentUser = $this->ACL->getUser();
if ($currentUser['role']['perm_admin']) {
return true;
}
if ($currentUser['role']['perm_org_admin'] && $currentUser['organisation']['id'] == $orgId) {
return true;
}
return false;
}
}

View File

@ -120,6 +120,12 @@ class UsersController extends AppController
if (Configure::read('keycloak.enabled')) {
$this->Users->enrollUserRouter($data);
}
},
'afterFind' => function ($user, &$params) use ($currentUser) {
if (!empty($user)) { // We don't have a 404
$user = $this->fetchTable('PermissionLimitations')->attachLimitations($user);
}
return $user;
}
]);
$responsePayload = $this->CRUD->getResponsePayload();
@ -227,6 +233,14 @@ class UsersController extends AppController
if (!$this->ACL->canEditUser($currentUser, $user)) {
throw new MethodNotAllowedException(__('You cannot edit the given user.'));
}
$user = $this->fetchTable('PermissionLimitations')->attachLimitations($user);
}
return $user;
};
} else {
$params['afterFind'] = function ($user, &$params) use ($currentUser) {
if (!empty($user)) { // We don't have a 404
$user = $this->fetchTable('PermissionLimitations')->attachLimitations($user);
}
return $user;
};

View File

@ -124,12 +124,15 @@ class IndividualsTable extends AppTable
public function getValidIndividualsToEdit(object $currentUser): array
{
$adminRoles = $this->Users->Roles->find('list')->select(['id'])->where(['perm_admin' => 1])->all()->toArray();
$validRoles = $this->Users->Roles->find('list')->select(['id'])->where(['perm_admin' => 0, 'perm_org_admin' => 0])->all()->toArray();
$validIndividualIds = $this->Users->find('list')->select(['individual_id'])->where(
[
'organisation_id' => $currentUser['organisation_id'],
'disabled' => 0,
'role_id NOT IN' => array_keys($adminRoles)
'OR' => [
['role_id IN' => array_keys($validRoles)],
['id' => $currentUser['id']],
]
]
)->all()->toArray();
return array_keys($validIndividualIds);

View File

@ -7,6 +7,7 @@ use App\Model\Entity\MetaTemplateNameDirectory;
use App\Model\Table\AppTable;
use Cake\ORM\RulesChecker;
use Cake\Validation\Validator;
use Cake\Log\Log;
class MetaTemplateNameDirectoryTable extends AppTable
{
@ -20,6 +21,9 @@ class MetaTemplateNameDirectoryTable extends AppTable
'foreignKey' => 'meta_template_directory_id',
]
);
$this->hasOne('MetaTemplates', [
'foreignKey' => 'meta_template_directory_id',
]);
$this->setDisplayField('name');
}
@ -59,7 +63,11 @@ class MetaTemplateNameDirectoryTable extends AppTable
if (!empty($existingTemplate)) {
return $existingTemplate;
}
$this->save($metaTemplateDirectory);
return $metaTemplateDirectory;
$savedEntity = $this->save($metaTemplateDirectory);
if ($savedEntity) {
return $metaTemplateDirectory;
}
Log::error(__('Could not save meta_template_directory. Reasons: {0}', json_encode($metaTemplateDirectory->getErrors())));
return false;
}
}

View File

@ -555,6 +555,10 @@ class MetaTemplatesTable extends AppTable
if (substr($file, -5) === '.json') {
$errorMessage = '';
$template = $this->decodeTemplateFromDisk($path . $file, $errorMessage);
if (!empty($errorMessage)) {
$error = $errorMessage;
return null;
}
if (!empty($template) && $template['uuid'] == $uuid) {
return $template;
}
@ -1318,6 +1322,13 @@ class MetaTemplatesTable extends AppTable
$updateStatus['next_version'] = $template['version'];
$updateStatus['new'] = false;
$updateStatus['automatically-updateable'] = false;
$updateStatus['conflicts'] = [];
if (empty($template)) {
$updateStatus['up-to-date'] = false;
$updateStatus['automatically-updateable'] = false;
$updateStatus['can-be-removed'] = false;
return $updateStatus;
}
if (intval($metaTemplate->version) >= intval($template['version'])) {
$updateStatus['up-to-date'] = true;
$updateStatus['conflicts'][] = __('Could not update the template. Local version is equal or newer.');

View File

@ -30,13 +30,17 @@ class PermissionLimitationsTable extends AppTable
public function getListOfLimitations(\App\Model\Entity\User $data)
{
$Users = TableRegistry::getTableLocator()->get('Users');
$ownOrgUserIds = $Users->find('list', [
'keyField' => 'id',
'valueField' => 'id',
'conditions' => [
'organisation_id' => $data['organisation_id']
]
])->all()->toList();
$includeOrganisationPermissions = !empty($data['organisation_id']);
$ownOrgUserIds = [];
if ($includeOrganisationPermissions) {
$ownOrgUserIds = $Users->find('list', [
'keyField' => 'id',
'valueField' => 'id',
'conditions' => [
'organisation_id' => $data['organisation_id']
]
])->all()->toList();
}
$MetaFields = TableRegistry::getTableLocator()->get('MetaFields');
$raw = $this->find()->select(['scope', 'permission', 'max_occurrence'])->disableHydration()->toArray();
$limitations = [];
@ -70,9 +74,12 @@ class PermissionLimitationsTable extends AppTable
if (!empty($ownOrgUserIds)) {
$conditions['parent_id IN'] = array_values($ownOrgUserIds);
}
$limitations[$field]['organisation']['current'] = $MetaFields->find('all', [
'conditions' => $conditions,
])->count();
$limitations[$field]['organisation']['current'] = '?';
if ($includeOrganisationPermissions) {
$limitations[$field]['organisation']['current'] = $MetaFields->find('all', [
'conditions' => $conditions,
])->count();
}
}
}
return $limitations;
@ -89,34 +96,35 @@ class PermissionLimitationsTable extends AppTable
if (!empty($data['MetaTemplates'])) {
foreach ($data['MetaTemplates'] as &$metaTemplate) {
foreach ($metaTemplate['meta_template_fields'] as &$meta_template_field) {
$boolean = $meta_template_field['type'] === 'boolean';
foreach ($meta_template_field['metaFields'] as &$metaField) {
if (isset($permissionLimitations[$metaField['field']])) {
foreach ($permissionLimitations[$metaField['field']] as $scope => $value) {
$messageType = 'warning';
if (isset($permissionLimitations[$meta_template_field['field']])) {
foreach ($permissionLimitations[$meta_template_field['field']] as $scope => $value) {
$messageType = 'warning';
if ($value['current'] == '?') {
$messageType = 'info';
} else {
if ($value['limit'] > $value['current']) {
$messageType = 'info';
}
if ($value['limit'] < $value['current']) {
$messageType = 'danger';
}
if (empty($metaField[$messageType])) {
$metaField[$messageType] = '';
}
$altText = __(
'There is a limitation enforced on the number of users with this permission {0}. Currently {1} slot(s) are used up of a maximum of {2} slot(s).',
$scope === 'global' ? __('instance wide') : __('for your organisation'),
$value['current'],
$value['limit']
);
$metaField[$messageType] .= sprintf(
' <span title="%s"><span class="text-dark"><i class="fas fa-%s"></i>: </span>%s/%s</span>',
$altText,
$icons[$scope],
$value['current'],
$value['limit']
);
}
if (empty($metaField[$messageType])) {
$metaField[$messageType] = '';
}
$altText = __(
'There is a limitation enforced on the number of users with this permission {0}. Currently {1} slot(s) are used up of a maximum of {2} slot(s).',
$scope === 'global' ? __('instance wide') : __('for your organisation'),
$value['current'],
$value['limit']
);
$meta_template_field[$messageType] .= sprintf(
' <span title="%s"><span class="text-dark"><i class="fas fa-%s"></i>: </span>%s/%s</span>',
$altText,
$icons[$scope],
$value['current'],
$value['limit']
);
}
}
}

View File

@ -321,7 +321,7 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
2 => __('Debug On + SQL Dump'),
],
'test' => function ($value, $setting, $validator) {
$validator->range('value', [0, 3]);
$validator->range('value', [0, 2]);
return testValidator($value, $validator);
},
],

View File

@ -73,13 +73,6 @@ class UsersTable extends AppTable
if (!$entity->isNew()) {
$success = $this->handleUserUpdateRouter($entity);
}
$permissionRestrictionCheck = $this->checkPermissionRestrictions($entity);
if ($permissionRestrictionCheck !== true) {
$entity->setErrors($permissionRestrictionCheck);
$event->stopPropagation();
$event->setResult(false);
return false;
}
return $success;
}
@ -187,10 +180,24 @@ class UsersTable extends AppTable
public function buildRules(RulesChecker $rules): RulesChecker
{
$rules->add($rules->isUnique(['username']));
$allowDuplicateIndividuals = false;
if (empty(Configure::read('user.multiple-users-per-individual')) || !empty(Configure::read('keycloak.enabled'))) {
$rules->add($rules->isUnique(['individual_id']));
}
$rules->add(function($entity, $options) {
$permissionRestrictionCheck = $this->checkPermissionRestrictions($entity);
if ($permissionRestrictionCheck !== true) {
foreach ($permissionRestrictionCheck as $permission_name => $errors) {
foreach ($entity->meta_fields as $i => $metaField) {
if ($metaField['field'] === $permission_name) {
$entity->meta_fields[$i]->setErrors(['value' => $errors]);
}
}
}
return false;
}
return true;
}, 'permissionLimitations');
return $rules;
}

View File

@ -21,6 +21,7 @@ class BootstrapIcon extends BootstrapGeneric
{
private $icon = '';
private $defaultOptions = [
'id' => '',
'class' => [],
'title' => '',
'attrs' => [],
@ -48,6 +49,7 @@ class BootstrapIcon extends BootstrapGeneric
{
$html = $this->node('span', array_merge(
[
'id' => $this->options['id'] ?? '',
'class' => array_merge(
$this->options['class'],
["fa fa-{$this->icon}"]

View File

@ -215,8 +215,6 @@ class BootstrapTabs extends BootstrapGeneric
],
[
"bg-{$this->options['header-variant']}",
"text-{$this->options['header-text-variant']}",
"border-{$this->options['header-border-variant']}"
]
)], $this->genNav());
$content = $this->node('div', ['class' => array_merge(
@ -226,7 +224,6 @@ class BootstrapTabs extends BootstrapGeneric
],
[
"bg-{$this->options['body-variant']}",
"text-{$this->options['body-text-variant']}"
]
)], $this->genContent());
@ -238,7 +235,6 @@ class BootstrapTabs extends BootstrapGeneric
($this->options['vertical-size'] == 'auto' ? 'flex-nowrap' : '')
],
[
"border-{$this->options['header-border-variant']}"
]
)], $containerContent);
return $container;

View File

@ -450,7 +450,7 @@ class BootstrapGeneric
return sprintf('%s="%s"', h($key), (!empty($escape) ? h($value) : $value));
}
return '';
} else if (empty($value)) {
} else if (!isset($value)) {
return '';
}
return sprintf('%s="%s"', h($key), (!empty($escape) ? h($value) : $value));

View File

@ -8,6 +8,15 @@ class FormFieldMassageHelper extends Helper
{
public function prepareFormElement(\Cake\View\Helper\FormHelper $form, array $controlParams, array $fieldData): string
{
if ($fieldData['tooltip']) {
$form->setTemplates([
'label' => '{{text}}{{tooltip}}',
]);
$controlParams['templateVars'] = array_merge(
$controlParams['templateVars'] ?? [],
['tooltip' => $fieldData['tooltip'], ]
);
}
if (!empty($fieldData['stateDependence'])) {
$controlParams['data-dependence-source'] = h($fieldData['stateDependence']['source']);
$controlParams['data-dependence-option'] = h($fieldData['stateDependence']['option']);

View File

@ -14,7 +14,8 @@ echo $this->element('genericElements/Form/genericForm', [
],
[
'field' => 'expiration',
'label' => 'Expiration'
'label' => __('Expiration'),
'type' => 'datetime',
]
],
'submit' => [

View File

@ -103,6 +103,9 @@ echo $this->element('genericElements/IndexTable/index_table', [
'icon' => 'trash',
'complex_requirement' => [
'function' => function ($row, $options) use ($loggedUser) {
if (!empty($row['user'])) { // cannot delete individuals with associated user(s)
return false;
}
return (bool)$loggedUser['role']['perm_admin'];
}
]

View File

@ -31,6 +31,7 @@ echo $this->element(
[
'key' => __('Tags'),
'type' => 'tags',
'editable' => $canEdit,
],
[
'key' => __('Alignments'),

View File

@ -10,7 +10,6 @@ echo $this->element('genericElements/Form/genericForm', [
'multiple' => true,
'select2' => true,
'label' => __('Members'),
'class' => 'select2-input',
'options' => $dropdownData['individuals']
],
[

View File

@ -0,0 +1,60 @@
<?php
use Cake\Utility\Hash;
echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'data' => $data,
'top_bar' => [
'children' => [
[
'type' => 'search',
'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value'
]
]
],
'fields' => [
[
'name' => '#',
'sort' => 'id',
'data_path' => 'id',
],
[
'name' => __('Name'),
'sort' => 'name',
'data_path' => 'name',
],
[
'name' => __('Namespace'),
'sort' => 'namespace',
'data_path' => 'namespace',
],
[
'name' => __('UUID'),
'sort' => 'uuid',
'data_path' => 'uuid'
],
[
'name' => __('Version'),
'sort' => 'version',
'data_path' => 'version',
],
[
'name' => __('Associated Meta-Template'),
'sort' => 'meta_template.id',
'data_path' => 'meta_template.id',
'element' => 'function',
'function' => function($row, $viewContext) {
return $viewContext->Bootstrap::node('a', [
'href' => h($baseurl . '/metaTemplates/view/' . $row->meta_template->id ?? ''),
], !empty($row->meta_template->name) ? (sprintf('%s (v%s)', h($row->meta_template->name), h($row->meta_template->version))) :'');
}
],
],
'title' => __('Meta Template Name Directory'),
'description' => __('The directory of all meta templates known by the system.'),
'actions' => []
]
]);

View File

@ -11,6 +11,14 @@ if ($updateStatus['up-to-date']) {
'dismissible' => false,
]);
$modalType = 'ok-only';
} else if (empty($templateOnDisk)) {
$diskTemplateError = $templateStatus['error'] ?? __('Unknown');
$bodyHtml .= $this->Bootstrap->alert([
'variant' => 'danger',
'html' => sprintf('<strong>%s</strong> %s<p>%s</p>', __('Could not get template on disk.'), __('Reason:'), h($diskTemplateError)),
'dismissible' => false,
]);
$modalType = 'ok-only';
} else {
if ($updateStatus['automatically-updateable']) {
$bodyHtml .= $this->Bootstrap->alert([

View File

@ -57,7 +57,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
'data_path' => 'url',
],
[
'name' => __('Nationality'),
'name' => __('Country'),
'data_path' => 'nationality',
],
[

View File

@ -10,12 +10,14 @@
array(
'field' => 'uuid',
'label' => 'UUID',
'type' => 'uuid'
'type' => 'uuid',
'tooltip' => __('If the Organisation already has a known UUID in another application such as MISP or another Cerebrate, please re-use this one.'),
),
array(
'field' => 'url'
),
array(
'label' => __('Country'),
'field' => 'nationality'
),
array(

View File

@ -66,7 +66,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
'data_path' => 'url',
],
[
'name' => __('Nationality'),
'name' => __('Country'),
'data_path' => 'nationality',
'sort' => 'nationality',
],

View File

@ -22,7 +22,7 @@ echo $this->element(
'path' => 'url'
],
[
'key' => __('Nationality'),
'key' => __('Country'),
'path' => 'nationality'
],
[
@ -40,6 +40,7 @@ echo $this->element(
[
'key' => __('Tags'),
'type' => 'tags',
'editable' => $canEdit,
],
[
'key' => __('Alignments'),

View File

@ -1,28 +1,31 @@
<?php
$topbarChildren = [];
if (!empty($loggedUser->role->perm_admin)) {
$topbarChildren[] = [
'type' => 'simple',
'children' => [
'data' => [
'type' => 'simple',
'text' => __('Add role'),
'class' => 'btn btn-primary',
'popover_url' => '/roles/add'
]
]
];
}
$topbarChildren[] = [
'type' => 'search',
'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value'
];
echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'data' => $data,
'top_bar' => [
'children' => [
[
'type' => 'simple',
'children' => [
'data' => [
'type' => 'simple',
'text' => __('Add role'),
'class' => 'btn btn-primary',
'popover_url' => '/roles/add'
]
]
],
[
'type' => 'search',
'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value'
]
]
'children' => $topbarChildren,
],
'fields' => [
[

View File

@ -1,99 +1,145 @@
<?php
use Cake\Core\Configure;
$passwordRequired = false;
$showPasswordField = false;
if ($this->request->getParam('action') === 'add') {
$dropdownData['individual'] = ['new' => __('New individual')] + $dropdownData['individual'];
if (!Configure::check('password_auth.enabled') || Configure::read('password_auth.enabled')) {
$passwordRequired = 'required';
}
}
use Cake\Core\Configure;
$passwordRequired = false;
$showPasswordField = false;
if ($this->request->getParam('action') === 'add') {
$dropdownData['individual'] = ['new' => __('New individual')] + $dropdownData['individual'];
if (!Configure::check('password_auth.enabled') || Configure::read('password_auth.enabled')) {
$showPasswordField = true;
$passwordRequired = 'required';
}
echo $this->element('genericElements/Form/genericForm', [
'data' => [
'description' => __('Roles define global rules for a set of users, including first and foremost access controls to certain functionalities.'),
'model' => 'Roles',
'fields' => [
[
'field' => 'individual_id',
'type' => 'dropdown',
'label' => __('Associated individual'),
'options' => isset($dropdownData['individual']) ? $dropdownData['individual'] : [],
'conditions' => $this->request->getParam('action') === 'add'
],
[
'field' => 'individual.email',
'stateDependence' => [
'source' => '#individual_id-field',
'option' => 'new'
],
'required' => false
],
[
'field' => 'individual.first_name',
'label' => 'First name',
'stateDependence' => [
'source' => '#individual_id-field',
'option' => 'new'
],
'required' => false
],
[
'field' => 'individual.last_name',
'label' => 'Last name',
'stateDependence' => [
'source' => '#individual_id-field',
'option' => 'new'
],
'required' => false
],
[
'field' => 'username',
'autocomplete' => 'off'
],
[
'field' => 'organisation_id',
'type' => 'dropdown',
'label' => __('Associated organisation'),
'options' => $dropdownData['organisation'],
'default' => $loggedUser['organisation_id']
],
[
'field' => 'password',
'label' => __('Password'),
'type' => 'password',
'required' => $passwordRequired,
'autocomplete' => 'new-password',
'value' => '',
'requirements' => $showPasswordField,
],
[
'field' => 'confirm_password',
'label' => __('Confirm Password'),
'type' => 'password',
'required' => $passwordRequired,
'autocomplete' => 'off',
'requirements' => $showPasswordField,
],
[
'field' => 'role_id',
'type' => 'dropdown',
'label' => __('Role'),
'options' => $dropdownData['role'],
'default' => $defaultRole ?? null
],
[
'field' => 'disabled',
'type' => 'checkbox',
'label' => 'Disable'
],
}
if (!Configure::check('password_auth.enabled') || Configure::read('password_auth.enabled')) {
$showPasswordField = true;
}
echo $this->element('genericElements/Form/genericForm', [
'data' => [
'description' => __('Roles define global rules for a set of users, including first and foremost access controls to certain functionalities.'),
'model' => 'Roles',
'fields' => [
[
'field' => 'individual_id',
'type' => 'dropdown',
'label' => __('Associated individual'),
'options' => isset($dropdownData['individual']) ? $dropdownData['individual'] : [],
'conditions' => $this->request->getParam('action') === 'add'
],
'submit' => [
'action' => $this->request->getParam('action')
]
[
'field' => 'individual.email',
'stateDependence' => [
'source' => '#individual_id-field',
'option' => 'new'
],
'required' => false
],
[
'field' => 'individual.first_name',
'label' => 'First name',
'stateDependence' => [
'source' => '#individual_id-field',
'option' => 'new'
],
'required' => false
],
[
'field' => 'individual.last_name',
'label' => 'Last name',
'stateDependence' => [
'source' => '#individual_id-field',
'option' => 'new'
],
'required' => false
],
[
'field' => 'username',
'autocomplete' => 'off'
],
[
'field' => 'organisation_id',
'type' => 'dropdown',
'label' => __('Associated organisation'),
'options' => $dropdownData['organisation'],
'default' => $loggedUser['organisation_id']
],
[
'field' => 'password',
'label' => __('Password'),
'type' => 'password',
'required' => $passwordRequired,
'autocomplete' => 'new-password',
'value' => '',
'requirements' => $showPasswordField,
],
[
'field' => 'confirm_password',
'label' => __('Confirm Password'),
'type' => 'password',
'required' => $passwordRequired,
'autocomplete' => 'off',
'requirements' => $showPasswordField,
],
[
'field' => 'role_id',
'type' => 'dropdown',
'label' => __('Role'),
'options' => $dropdownData['role'],
'default' => $defaultRole ?? null
],
[
'field' => 'disabled',
'type' => 'checkbox',
'label' => 'Disable'
],
],
'submit' => [
'action' => $this->request->getParam('action')
]
]);
]
]);
?>
</div>
<script>
$(document).ready(function() {
const entity = <?= json_encode($entity) ?>;
console.log(entity);
if (entity.MetaTemplates) {
for (const [metaTemplateId, metaTemplate] of Object.entries(entity.MetaTemplates)) {
for (const [metaTemplateFieldId, metaTemplateField] of Object.entries(metaTemplate.meta_template_fields)) {
let metaFieldId = false
if (metaTemplateField.metaFields !== undefined && Object.keys(metaTemplateField.metaFields).length > 0) {
metaFieldId = Object.keys(metaTemplateField.metaFields)[0]
}
let metafieldInput
const baseQueryPath = `MetaTemplates.${metaTemplateId}.meta_template_fields.${metaTemplateFieldId}.metaFields`
if (metaFieldId) {
metafieldInput = document.getElementById(`${baseQueryPath}.${metaFieldId}.value-field`)
} else {
metafieldInput = document.getElementById(`${baseQueryPath}.new.0-field`)
}
if (metafieldInput !== null) {
const permissionWarnings = buildPermissionElement(metaTemplateField)
$(metafieldInput.parentElement).append(permissionWarnings)
}
}
}
}
function buildPermissionElement(metaTemplateField) {
const warningTypes = ['danger', 'warning', 'info', ]
const $span = $('<span>').addClass('ms-2')
warningTypes.forEach(warningType => {
if (metaTemplateField[warningType]) {
$theWarning = $('<span>')
.addClass([
`text-${warningType}`,
'ms-1',
])
.append($(metaTemplateField[warningType]))
$span.append($theWarning)
}
});
return $span
}
})
</script>

View File

@ -2,7 +2,7 @@
$seed = 's-' . mt_rand();
$controlParams = [
'type' => 'select',
'options' => $fieldData['options'],
'options' => $fieldData['options'] ?? [],
'empty' => $fieldData['empty'] ?? false,
'value' => $fieldData['value'] ?? null,
'multiple' => $fieldData['multiple'] ?? false,
@ -19,9 +19,13 @@ if (!empty($fieldData['label'])) {
if ($controlParams['options'] instanceof \Cake\ORM\Query) {
$controlParams['options'] = $controlParams['options']->all()->toList();
}
if (!empty($fieldData['select2'])) {
$initSelect2 = false;
if (isset($fieldData['select2']) && $fieldData['select2'] == true) {
$initSelect2 = true;
$fieldData['select2'] = $fieldData['select2'] === true ? [] : $fieldData['select2'];
$controlParams['class'] .= ' select2-input';
}
$controlParams['class'] .= ' dropdown-custom-value' . "-$seed";
if (in_array('_custom', array_keys($controlParams['options']))) {
$customInputValue = $this->Form->getSourceValue($fieldData['field']);
if (!in_array($customInputValue, $controlParams['options'])) {
@ -34,7 +38,6 @@ if (in_array('_custom', array_keys($controlParams['options']))) {
} else {
$customInputValue = '';
}
$controlParams['class'] .= ' dropdown-custom-value' . "-$seed";
$adaptedField = $fieldData['field'] . '_custom';
$controlParams['templates']['formGroup'] = sprintf(
'<label class="col-sm-2 col-form-label form-label" {{attrs}}>{{label}}</label><div class="col-sm-10 multi-metafield-input-container"><div class="d-flex form-dropdown-with-freetext input-group">{{input}}{{error}}%s</div></div>',
@ -52,14 +55,18 @@ echo $this->FormFieldMassage->prepareFormElement($this->Form, $controlParams, $f
$select.attr('onclick', 'toggleFreetextSelectField(this)')
$select.parent().find('input.custom-value').attr('oninput', 'updateAssociatedSelect(this)')
updateAssociatedSelect($select.parent().find('input.custom-value')[0])
<?php if (!empty($fieldData['select2'])) : ?>
let $container = $select.closest('.modal-dialog')
<?php if ($initSelect2) : ?>
// let $container = $select.closest('.modal-dialog .modal-body')
let $container = []
if ($container.length == 0) {
$container = $(document.body)
}
$select.select2({
const defaultSelect2Options = {
dropdownParent: $container,
})
}
const passedSelect2Options = <?= json_encode($fieldData['select2']) ?>;
const select2Options = Object.assign({}, passedSelect2Options, defaultSelect2Options)
$select.select2(select2Options)
<?php endif; ?>
})

View File

@ -1,43 +1,43 @@
<?php
$random = Cake\Utility\Security::randomString(8);
$params['div'] = false;
$this->Form->setTemplates([
'inputContainer' => '{{content}}',
'inputContainerError' => '{{content}}',
'formGroup' => '{{input}}',
]);
$label = $fieldData['label'];
$formElement = $this->FormFieldMassage->prepareFormElement($this->Form, $params, $fieldData);
$temp = sprintf(
'<div class="row mb-3">
<div class="col-sm-2 form-label">%s</div>
<div class="col-sm-10">
<div class="input-group">
%s<span>%s</span>
</div>
</div>
</div>',
h($label),
$formElement,
sprintf(
'<span id="uuid-gen-%s" class="btn btn-secondary">%s</span>',
$random,
__('Generate')
)
);
echo $temp;
$random = Cake\Utility\Security::randomString(8);
$params['div'] = false;
$genUUIDButton = $this->Bootstrap->button([
'id' => "uuid-gen-{$random}",
'variant' => 'secondary',
'text' => __('Generate'),
]);
$this->Form->setTemplates([
'input' => sprintf('<div class="input-group">%s{{genUUIDButton}}</div>', $this->Form->getTemplates('input')),
]);
$params['templateVars'] = [
'genUUIDButton' => $genUUIDButton,
];
$formElement = $this->FormFieldMassage->prepareFormElement($this->Form, $params, $fieldData);
echo $formElement;
?>
<script type="text/javascript">
$(document).ready(function() {
$('#uuid-gen-<?= h($random) ?>').on('click', function() {
$.ajax({
success:function (data, textStatus) {
$('#uuid-field').val(data["uuid"]);
},
type: "get",
cache: false,
url: "/organisations/generateUUID",
});
});
const $node = $('#uuid-gen-<?= h($random) ?>')
$node.click(fetchUUID)
function fetchUUID() {
const urlGet = '/organisations/generateUUID'
const options = {
statusNode: $node,
}
return AJAXApi.quickFetchJSON(urlGet, options)
.then(function(data) {
$('#uuid-field').val(data["uuid"])
})
.catch((e) => {
UI.toast({
variant: 'danger',
text: '<?= __('Could not generate UUID') ?>'
})
})
}
});
</script>
</script>

View File

@ -11,11 +11,12 @@
$fieldData['label'] = \Cake\Utility\Inflector::humanize($fieldData['field']);
}
}
if (!empty($fieldDesc[$fieldData['field']])) {
$fieldData['label'] .= $this->element(
$fieldDescription = $fieldData['tooltip'] ?? ($fieldDesc[$fieldData['field']] ?? false);
if (!empty($fieldDescription)) {
$fieldData['tooltip'] = $this->element(
'genericElements/Form/formInfo', array(
'field' => $fieldData,
'fieldDesc' => $fieldDesc[$fieldData['field']],
'fieldDesc' => $fieldDescription,
'modelForForm' => $modelForForm
)
);

View File

@ -1,4 +1,5 @@
<?php
$seed = mt_rand();
if (!is_array($fieldDesc)) {
$fieldDesc = array('info' => $fieldDesc);
$default = 'info';
@ -16,32 +17,46 @@
$default = 'info';
}
}
echo sprintf(
'<span id = "%sInfoPopover" class="icon-info-sign" data-bs-toggle="popover" data-bs-trigger="hover"></span>',
h($field['field'])
);
$popoverID = sprintf("%sInfoPopover%s", h($field['field']), $seed);
echo $this->Bootstrap->icon('info-circle', [
'id' => $popoverID,
'class' => ['ms-1'],
'attrs' => [
'data-bs-toggle' => 'popover',
'data-bs-trigger' => 'hover',
]
]);
?>
<script type="text/javascript">
$(document).ready(function() {
new bootstrap.Popover('#<?php echo h($field['field']); ?>InfoPopover', {
new bootstrap.Popover('#<?= $popoverID ?>', {
html: true,
content: function() {
var tempSelector = '#<?php echo h($modelForForm . \Cake\Utility\Inflector::camelize($field['field'])); ?>';
if ($(tempSelector)[0].nodeName === "SELECT" && Object.keys(fieldDesc).length > 1) {
return $('<div>').append(
$('<span>').attr('class', 'blue bold').text($(tempSelector +" option:selected").text())
).append(
$('<span>').text(': ' + fieldDesc[$(tempSelector).val()])
return $('<div>')
.append(
$('<span>')
.attr('class', 'text-primary fw-bold')
.text('<?php echo h(\Cake\Utility\Inflector::humanize($field['field'])); ?>')
)
.append(
$('<span>').text(": <?= h($fieldDesc["info"]) ?>")
);
} else {
return $('<div>').append(
$('<span>').attr('class', 'blue bold').text('<?php echo h(\Cake\Utility\Inflector::humanize($field['field'])); ?>')
).append(
$('<span>').text(": " + fieldDesc["info"])
);
}
// var tempSelector = '#<?php echo h($modelForForm . \Cake\Utility\Inflector::camelize($field['field'])); ?>';
// if ($(tempSelector)[0].nodeName === "SELECT" && Object.keys(fieldDesc).length > 1) {
// return $('<div>').append(
// $('<span>').attr('class', 'blue bold').text($(tempSelector +" option:selected").text())
// ).append(
// $('<span>').text(': ' + fieldDesc[$(tempSelector).val()])
// );
// } else {
// return $('<div>').append(
// $('<span>').attr('class', 'blue bold').text('<?php echo h(\Cake\Utility\Inflector::humanize($field['field'])); ?>')
// ).append(
// $('<span>').text(": " + fieldDesc["info"])
// );
// }
}
});
var fieldDesc = <?php echo json_encode($fieldDesc); ?>;
// var fieldDesc = <?php echo json_encode($fieldDesc); ?>;
});
</script>

View File

@ -19,7 +19,7 @@
$entity = isset($entity) ? $entity : null;
$fieldsString = '';
$simpleFieldWhitelist = [
'default', 'type', 'placeholder', 'label', 'empty', 'rows', 'div', 'required', 'templates', 'options', 'value', 'checked'
'default', 'type', 'placeholder', 'label', 'empty', 'rows', 'div', 'required', 'templates', 'options', 'value', 'checked',
];
if (empty($data['url'])) {
$data['url'] = ["controller" => $this->request->getParam('controller'), "action" => $this->request->getParam('url')];
@ -155,14 +155,5 @@
$('.formDropdown').on('change', function() {
executeStateDependencyChecks('#' + this.id);
})
<?php if (!empty($initSelect2)): ?>
<?php
$dropdownParent = !empty($seedModal) ? sprintf("$('.modal-dialog.%s .modal-body')", $seedModal) : "$(document.body)";
?>
$('select.select2-input').select2({
dropdownParent: <?= $dropdownParent ?>,
width: '100%',
})
<?php endif; ?>
});
</script>

View File

@ -4,7 +4,7 @@ use Cake\Utility\Inflector;
$default_template = [
'inputContainer' => '<div class="row mb-3 metafield-container">{{content}}</div>',
'inputContainerError' => '<div class="row mb-3 metafield-container has-error">{{content}}</div>',
'inputContainerError' => '<div class="row mb-3 metafield-container has-error">{{content}}{{error}}</div>',
'formGroup' => '<label class="col-sm-2 col-form-label form-label" {{attrs}}>{{label}}</label><div class="col-sm-10">{{input}}{{error}}</div>',
'error' => '<div class="error-message invalid-feedback d-block">{{content}}</div>',
'errorList' => '<ul>{{content}}</ul>',

View File

@ -3,6 +3,6 @@
$tags = Cake\Utility\Hash::get($data, 'tags');
echo $this->Tag->tags($tags, [
'allTags' => $allTags,
'picker' => true,
'editable' => true,
'picker' => !empty($field['editable']),
'editable' => !empty($field['editable']),
]);

View File

@ -28,9 +28,9 @@ foreach($data['MetaTemplates'] as $metaTemplate) {
]
]),
'rawNoEscaping' => true,
'warning' => $metaField->warning ?? null,
'info' => $metaField->info ?? null,
'danger' => $metaField->danger ?? null
'notice_warning' => $metaTemplateField->warning ?? null,
'notice_info' => $metaTemplateField->info ?? null,
'notice_danger' => $metaTemplateField->danger ?? null
];
$labelPrintedOnce = true;
}

View File

@ -81,6 +81,8 @@ if (!empty($breadcrumb)) {
}
if (!empty($actionEntry['isPOST'])) {
$onclickFunction = sprintf('UI.overlayUntilResolve(this, UI.submissionModalAutoGuess(\'%s\'))', h(Router::url($actionEntry['url'])));
} else if (!empty($actionEntry['isRedirect'])) {
$onclickFunction = sprintf('window.location.replace(\'%s\');', h(Router::url($actionEntry['url'])));
} else {
$onclickFunction = sprintf('UI.overlayUntilResolve(this, UI.modalFromUrl(\'%s\'))', h(Router::url($actionEntry['url'])));
}

View File

@ -47,19 +47,28 @@ $filteringForm = $this->Bootstrap->table(
__('Value'),
sprintf('<sup class="fa fa-info" title="%s"><sup>', __('Supports strict matches and LIKE matches with the `%` character.&#10;Example: `%.com`'))
),
'formatter' => function ($field, $row) use ($typeMap, $formTypeMap) {
'formatter' => function ($field, $row) use ($typeMap, $formTypeMap, $filtersConfig) {
$fieldName = $row['fieldname'];
$formType = $formTypeMap[$typeMap[$fieldName]] ?? 'text';
$fieldData = [
'field' => $fieldName,
'type' => $formType,
'label' => '',
'class' => 'fieldValue form-control-sm'
];
if (!empty($filtersConfig[$fieldName]['multiple'])) {
$fieldData['type'] = 'dropdown';
$fieldData['multiple'] = true;
$fieldData['select2'] = [
'tags' => true,
'tokenSeparators' => [',', ' '],
];
}
$this->Form->setTemplates([
'formGroup' => '<div class="col-sm-10">{{input}}</div>',
]);
return $this->element('genericElements/Form/fieldScaffold', [
'fieldData' => [
'field' => $fieldName,
'type' => $formType,
'label' => '',
'class' => 'fieldValue form-control-sm'
],
'fieldData' => $fieldData,
'params' => []
]);
}
@ -169,6 +178,36 @@ echo $this->Bootstrap->modal([
}
setFilteringValues($filteringTable, field, value, operator)
}
if (tags.length > 0) {
setFilteringTags($filteringTable, tags)
}
}
function setFilteringValues($filteringTable, field, value, operator) {
$row = $filteringTable.find('td > span.fieldName').filter(function() {
return $(this).data('fieldname') == field
}).closest('tr')
$row.find('.fieldOperator').val(operator)
const $formElement = $row.find('.fieldValue');
if ($formElement.attr('type') === 'datetime-local') {
$formElement.val(moment(value).format('yyyy-MM-DDThh:mm:ss'))
} else if ($formElement.is('select') && Array.isArray(value)) {
let newOptions = [];
value.forEach(aValue => {
const existingOption = $formElement.find('option').filter(function() {
return $(this).val() === aValue
})
if (existingOption.length == 0) {
newOptions.push(new Option(aValue, aValue, true, true))
}
})
$formElement.append(newOptions).trigger('change');
} else {
$formElement.val(value)
}
}
function setFilteringTags($filteringTable, tags) {
$select = $filteringTable.closest('.modal-body').find('select.select2-input')
let passedTags = []
tags.forEach(tagname => {
@ -185,19 +224,6 @@ echo $this->Bootstrap->modal([
.trigger('change')
}
function setFilteringValues($filteringTable, field, value, operator) {
$row = $filteringTable.find('td > span.fieldName').filter(function() {
return $(this).data('fieldname') == field
}).closest('tr')
$row.find('.fieldOperator').val(operator)
const $formElement = $row.find('.fieldValue');
if ($formElement.attr('type') === 'datetime-local') {
$formElement.val(moment(value).format('yyyy-MM-DDThh:mm:ss'))
} else {
$formElement.val(value)
}
}
function getDataFromRow($row) {
const rowData = {};
rowData['name'] = $row.find('td > span.fieldName').data('fieldname')

View File

@ -58,7 +58,7 @@ $sidebarOpen = $loggedUser->user_settings_by_name_with_fallback['ui.sidebar.expa
<?= $this->Html->css('CodeMirror/addon/hint/show-hint') ?>
<?= $this->Html->css('CodeMirror/addon/lint/lint') ?>
<?= $this->Html->css('select2.min') ?>
<?= $this->Html->css('select2-bootstrap5') ?>
<?= $this->Html->css('select2-bootstrap5-vars') ?>
<?= $this->Html->script('apexcharts.min') ?>
<?= $this->Html->script('moment-with-locales.min') ?>
<?= $this->Html->css('apexcharts') ?>

View File

@ -94,7 +94,7 @@ input[type="checkbox"]:disabled.change-cursor {
}
.select2-container {
z-index: 900;
z-index: 1060;
}
.select2-container--bootstrap-5 {

View File

@ -0,0 +1,516 @@
/*!
* Select2 v4 Bootstrap 5 theme v1.1.1
*/
.select2-container--bootstrap-5 {
display: block;
}
.select2-container--bootstrap-5 *:focus {
outline: 0;
}
.select2-container--bootstrap-5 .select2-selection {
width: 100%;
min-height: calc(1.5em + (0.75rem + 2px));
padding: 0.375rem 0.75rem;
font-family: inherit;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border: 1px solid var(--bs-gray-400);
border-radius: 0.25rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
@media (prefers-reduced-motion: reduce) {
.select2-container--bootstrap-5 .select2-selection {
transition: none;
}
}
.select2-container--bootstrap-5.select2-container--focus .select2-selection, .select2-container--bootstrap-5.select2-container--open .select2-selection {
border-color: rgba(var(--bs-primary-rgb), 0.5);
box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25);
}
.select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection {
border-bottom: 1px solid transparent;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection {
border-top: 1px solid transparent;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.select2-container--bootstrap-5 .select2-search {
width: 100%;
}
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear,
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear {
position: absolute;
top: 50%;
right: 2.25rem;
width: 0.75rem;
height: 0.75rem;
padding: 0.25em 0.25em;
overflow: hidden;
text-indent: 100%;
white-space: nowrap;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.75rem auto no-repeat;
transform: translateY(-50%);
}
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear:hover,
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.75rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear > span,
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear > span {
display: none;
}
.select2-container--bootstrap-5 .select2-dropdown {
border-color: var(--bs-gray-400);
border-radius: 0.25rem;
}
.select2-container--bootstrap-5 .select2-dropdown.select2-dropdown--below {
border-top: 0 solid transparent;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.select2-container--bootstrap-5 .select2-dropdown.select2-dropdown--above {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.select2-container--bootstrap-5 .select2-dropdown .select2-search {
padding: 0.375rem 0.75rem;
}
.select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field {
display: block;
width: 100%;
padding: 0.375rem 0.75rem;
font-family: inherit;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
background-clip: padding-box;
border: 1px solid var(--bs-gray-400);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: 0.25rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field {
transition: none;
}
}
.select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field:focus {
border-color: rgba(var(--bs-primary-rgb), 0.5);
box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25);
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options:not(.select2-results__options--nested) {
max-height: 15rem;
overflow-y: auto;
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option {
padding: 0.375rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__message {
color: var(--bs-secondary);
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__option--highlighted {
color: #000;
background-color: var(--bs-light);
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__option--selected, .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[aria-selected=true] {
color: var(--bs-body-color);
background-color: var(--bs-primary);
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__option--disabled, .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[aria-disabled=true] {
color: var(--bs-gray);
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] {
padding: 0;
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group {
padding: 0.375rem 0.375rem;
font-weight: 500;
line-height: 1.5;
color: var(--bs-gray);
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option {
padding: 0.375rem 0.75rem;
}
.select2-container--bootstrap-5 .select2-selection--single {
padding: 0.375rem 2.25rem 0.375rem 0.75rem;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size: 16px 12px;
}
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered {
padding: 0;
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
}
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered .select2-selection__placeholder {
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
}
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered .select2-selection__arrow {
display: none;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding-left: 0;
margin: 0;
list-style: none;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
display: flex;
flex-direction: row;
align-items: center;
padding: 0.35em 0.65em;
margin-right: 0.375rem;
margin-bottom: 0.375rem;
font-size: 1rem;
color: var(--bs-body-color);
cursor: auto;
border: 1px solid var(--bs-gray-400);
border-radius: 0.25rem;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove {
width: 0.75rem;
height: 0.75rem;
padding: 0.25em 0.25em;
margin-right: 0.25rem;
overflow: hidden;
text-indent: 100%;
white-space: nowrap;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.75rem auto no-repeat;
border: 0;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.75rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove > span {
display: none;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-search {
display: block;
width: 100%;
height: 1.5rem;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-search .select2-search__field {
width: 100%;
height: 1.5rem;
margin-top: 0;
margin-left: 0;
font-family: inherit;
line-height: 1.5;
background-color: transparent;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear {
right: 0.75rem;
}
.select2-container--bootstrap-5.select2-container--disabled .select2-selection, .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection {
color: var(--bs-secondary);
cursor: not-allowed;
background-color: var(--bs-gray-200);
border-color: var(--bs-gray-400);
box-shadow: none;
}
.select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__clear, .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__clear {
display: none;
}
.select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__choice, .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__choice {
cursor: not-allowed;
}
.select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__choice .select2-selection__choice__remove, .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__choice .select2-selection__choice__remove {
display: none;
}
.select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__rendered:not(:empty), .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__rendered:not(:empty) {
padding-bottom: 0;
}
.select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__rendered:not(:empty) + .select2-search, .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__rendered:not(:empty) + .select2-search {
display: none;
}
.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu).select2-container--bootstrap-5 .select2-selection {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu).select2-container--bootstrap-5 .select2-selection {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group > .input-group-text ~ .select2-container--bootstrap-5 .select2-selection,
.input-group > .btn ~ .select2-container--bootstrap-5 .select2-selection,
.input-group > .dropdown-menu ~ .select2-container--bootstrap-5 .select2-selection {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.input-group .select2-container--bootstrap-5 {
flex-grow: 1;
}
.input-group .select2-container--bootstrap-5 .select2-selection {
height: 100%;
}
.is-valid + .select2-container--bootstrap-5 .select2-selection, form.was-validated select:valid + .select2-container--bootstrap-5 .select2-selection {
border-color: var(--bs-success);
}
.is-valid + .select2-container--bootstrap-5.select2-container--focus .select2-selection, .is-valid + .select2-container--bootstrap-5.select2-container--open .select2-selection, form.was-validated select:valid + .select2-container--bootstrap-5.select2-container--focus .select2-selection, form.was-validated select:valid + .select2-container--bootstrap-5.select2-container--open .select2-selection {
border-color: var(--bs-success);
box-shadow: 0 0 0 0.25rem rgba( var(--bs-success-rgb), 0.25);
}
.is-valid + .select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection, form.was-validated select:valid + .select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection {
border-bottom: 1px solid transparent;
}
.is-valid + .select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection, form.was-validated select:valid + .select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection {
border-top: 1px solid transparent;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.is-invalid + .select2-container--bootstrap-5 .select2-selection, form.was-validated select:invalid + .select2-container--bootstrap-5 .select2-selection {
border-color: var(--bs-danger);
}
.is-invalid + .select2-container--bootstrap-5.select2-container--focus .select2-selection, .is-invalid + .select2-container--bootstrap-5.select2-container--open .select2-selection, form.was-validated select:invalid + .select2-container--bootstrap-5.select2-container--focus .select2-selection, form.was-validated select:invalid + .select2-container--bootstrap-5.select2-container--open .select2-selection {
border-color: var(--bs-danger);
box-shadow: 0 0 0 0.25rem rgba( var(--bs-danger-rgb), 0.25);
}
.is-invalid + .select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection, form.was-validated select:invalid + .select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection {
border-bottom: 1px solid transparent;
}
.is-invalid + .select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection, form.was-validated select:invalid + .select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection {
border-top: 1px solid transparent;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection {
min-height: calc(1.5em + (0.5rem + 2px));
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--single .select2-selection__clear,
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__clear {
width: 0.5rem;
height: 0.5rem;
padding: 0.125rem 0.125rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--single .select2-selection__clear:hover,
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__clear:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-dropdown .select2-search .select2-search__field {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-dropdown .select2-results__options .select2-results__option {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group {
padding: 0.25rem 0.25rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option {
padding: 0.25rem 0.5rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--single {
padding: 0.25rem 2.25rem 0.25rem 0.5rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__rendered:not(:empty) {
padding-bottom: 0.25rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
padding: 0.35em 0.65em;
font-size: 0.875rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove {
width: 0.5rem;
height: 0.5rem;
padding: 0.125rem 0.125rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__clear {
right: 0.5rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection {
min-height: calc(1.5em + (1rem + 2px));
padding: 0.5rem 1rem;
font-size: 1.25rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--single .select2-selection__clear,
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__clear {
width: 1rem;
height: 1rem;
padding: 0.5rem 0.5rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--single .select2-selection__clear:hover,
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__clear:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-dropdown .select2-search .select2-search__field {
padding: 0.5rem 1rem;
font-size: 1.25rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-dropdown .select2-results__options .select2-results__option {
padding: 0.5rem 1rem;
font-size: 1.25rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group {
padding: 0.5rem 0.5rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option {
padding: 0.5rem 1rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--single {
padding: 0.5rem 2.25rem 0.5rem 1rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__rendered:not(:empty) {
padding-bottom: 0.5rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
padding: 0.35em 0.65em;
font-size: 1.25rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove {
width: 1rem;
height: 1rem;
padding: 0.5rem 0.5rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__clear {
right: 1rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection {
min-height: calc(1.5em + (0.5rem + 2px));
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear,
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear {
width: 0.5rem;
height: 0.5rem;
padding: 0.125rem 0.125rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear:hover,
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group {
padding: 0.25rem 0.25rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option {
padding: 0.25rem 0.5rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--single {
padding: 0.25rem 2.25rem 0.25rem 0.5rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered:not(:empty) {
padding-bottom: 0.25rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
padding: 0.35em 0.65em;
font-size: 0.875rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove {
width: 0.5rem;
height: 0.5rem;
padding: 0.125rem 0.125rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear {
right: 0.5rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection {
min-height: calc(1.5em + (1rem + 2px));
padding: 0.5rem 1rem;
font-size: 1.25rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear,
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear {
width: 1rem;
height: 1rem;
padding: 0.5rem 0.5rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear:hover,
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field {
padding: 0.5rem 1rem;
font-size: 1.25rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option {
padding: 0.5rem 1rem;
font-size: 1.25rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group {
padding: 0.5rem 0.5rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option {
padding: 0.5rem 1rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--single {
padding: 0.5rem 2.25rem 0.5rem 1rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered:not(:empty) {
padding-bottom: 0.5rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
padding: 0.35em 0.65em;
font-size: 1.25rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove {
width: 1rem;
height: 1rem;
padding: 0.5rem 0.5rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear {
right: 1rem;
}