Merge branch 'develop-unstable' into develop
commit
b808705049
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Migrations\AbstractMigration;
|
||||
|
||||
final class InboxSeverity extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change(): void
|
||||
{
|
||||
$exists = $this->table('inbox')->hasColumn('severity');
|
||||
if (!$exists) {
|
||||
$this->table('inbox')
|
||||
->addColumn('severity', 'integer', [
|
||||
'null' => false,
|
||||
'default' => 0,
|
||||
'signed' => false,
|
||||
'length' => 10,
|
||||
])
|
||||
->renameColumn('comment', 'message')
|
||||
->removeColumn('description')
|
||||
->update();
|
||||
$this->table('outbox')
|
||||
->addColumn('severity', 'integer', [
|
||||
'null' => false,
|
||||
'default' => 0,
|
||||
'signed' => false,
|
||||
'length' => 10,
|
||||
])
|
||||
->renameColumn('comment', 'message')
|
||||
->removeColumn('description')
|
||||
->update();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Migrations\AbstractMigration;
|
||||
use Phinx\Db\Adapter\MysqlAdapter;
|
||||
|
||||
|
||||
class MetaFieldSaneDefault extends AbstractMigration
|
||||
{
|
||||
|
||||
public $autoId = false; // turn off automatic `id` column create. We want it to be `int(10) unsigned`
|
||||
|
||||
|
||||
public function change()
|
||||
{
|
||||
$exists = $this->table('meta_template_fields')->hasColumn('sane_default');
|
||||
if (!$exists) {
|
||||
$this->table('meta_template_fields')
|
||||
->addColumn('sane_default', 'text', [
|
||||
'default' => null,
|
||||
'null' => true,
|
||||
'limit' => MysqlAdapter::TEXT_LONG,
|
||||
'comment' => 'List of sane default values to be proposed',
|
||||
])
|
||||
->addColumn('values_list', 'text', [
|
||||
'default' => null,
|
||||
'null' => true,
|
||||
'limit' => MysqlAdapter::TEXT_LONG,
|
||||
'comment' => 'List of values that have to be used',
|
||||
])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,9 +19,12 @@ class GenericInboxProcessor
|
|||
protected $validator;
|
||||
protected $processingTemplate = '/genericTemplates/confirm';
|
||||
protected $processingTemplatesDirectory = ROOT . '/libraries/default/InboxProcessors/templates';
|
||||
protected $defaultSeverity;
|
||||
protected $severity;
|
||||
|
||||
public function __construct($registerActions=false) {
|
||||
$this->Inbox = TableRegistry::getTableLocator()->get('Inbox');
|
||||
$this->defaultSeverity = $this->Inbox::SEVERITY_INFO;
|
||||
if ($registerActions) {
|
||||
$this->registerActionInProcessor();
|
||||
}
|
||||
|
@ -54,6 +57,10 @@ class GenericInboxProcessor
|
|||
{
|
||||
return $this->description ?? '';
|
||||
}
|
||||
public function getSeverity()
|
||||
{
|
||||
return $this->severity ?? $this->defaultSeverity;
|
||||
}
|
||||
|
||||
protected function getProcessingTemplatePath()
|
||||
{
|
||||
|
@ -76,8 +83,9 @@ class GenericInboxProcessor
|
|||
$builder = new ViewBuilder();
|
||||
$builder->disableAutoLayout()
|
||||
->setClassName('Monad')
|
||||
->setTemplate($processingTemplate);
|
||||
$view = $builder->build($viewVariables);
|
||||
->setTemplate($processingTemplate)
|
||||
->setVars($viewVariables);
|
||||
$view = $builder->build();
|
||||
$view->setRequest($serverRequest);
|
||||
return $view->render();
|
||||
}
|
||||
|
@ -191,7 +199,7 @@ class GenericInboxProcessor
|
|||
{
|
||||
$requestData['scope'] = $this->scope;
|
||||
$requestData['action'] = $this->action;
|
||||
$requestData['description'] = $this->description;
|
||||
$requestData['severity'] = $this->getSeverity();
|
||||
$request = $this->generateRequest($requestData);
|
||||
$savedRequest = $this->Inbox->createEntry($request);
|
||||
return $this->genActionResult(
|
||||
|
|
|
@ -179,6 +179,7 @@ class IncomingConnectionRequestProcessor extends LocalToolInboxProcessor impleme
|
|||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->severity = $this->Inbox::SEVERITY_WARNING;
|
||||
$this->description = __('Handle Phase I of inter-connection when another cerebrate instance performs the request.');
|
||||
}
|
||||
|
||||
|
@ -291,6 +292,7 @@ class AcceptedRequestProcessor extends LocalToolInboxProcessor implements Generi
|
|||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->severity = $this->Inbox::SEVERITY_WARNING;
|
||||
$this->description = __('Handle Phase II of inter-connection when initial request has been accepted by the remote cerebrate.');
|
||||
}
|
||||
|
||||
|
@ -367,6 +369,7 @@ class DeclinedRequestProcessor extends LocalToolInboxProcessor implements Generi
|
|||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->severity = $this->Inbox::SEVERITY_WARNING;
|
||||
$this->description = __('Handle Phase II of MISP inter-connection when initial request has been declined by the remote cerebrate.');
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
require_once(ROOT . DS . 'libraries' . DS . 'default' . DS . 'InboxProcessors' . DS . 'GenericInboxProcessor.php');
|
||||
|
||||
class NotificationInboxProcessor extends GenericInboxProcessor
|
||||
{
|
||||
protected $scope = 'Notification';
|
||||
protected $action = 'not-specified'; //overriden when extending
|
||||
protected $description = ''; // overriden when extending
|
||||
protected $registeredActions = [
|
||||
'DataChange'
|
||||
];
|
||||
|
||||
public function __construct($loadFromAction=false) {
|
||||
parent::__construct($loadFromAction);
|
||||
}
|
||||
|
||||
public function create($requestData)
|
||||
{
|
||||
return parent::create($requestData);
|
||||
}
|
||||
}
|
||||
|
||||
class DataChangeProcessor extends NotificationInboxProcessor implements GenericInboxProcessorActionI {
|
||||
public $action = 'DataChange';
|
||||
protected $description;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->description = __('Notify when data has been changed');
|
||||
$this->severity = $this->Inbox::SEVERITY_PRIMARY;
|
||||
}
|
||||
|
||||
protected function addValidatorRules($validator)
|
||||
{
|
||||
return $validator
|
||||
->requirePresence('original', 'changed')
|
||||
->isArray('original', __('The `original` data must be provided'))
|
||||
->isArray('changed', __('The `changed` data must be provided'));
|
||||
}
|
||||
|
||||
public function create($requestData) {
|
||||
$this->validateRequestData($requestData);
|
||||
return parent::create($requestData);
|
||||
}
|
||||
|
||||
public function getViewVariables($request)
|
||||
{
|
||||
$request = $this->__decodeMoreFields($request);
|
||||
return [
|
||||
'data' => [
|
||||
'original' => $request->data['original'],
|
||||
'changed' => $request->data['changed'],
|
||||
'summaryTemplate' => $request->data['summaryTemplate'],
|
||||
'summaryMessage' => $request->data['summaryMessage'],
|
||||
'entityType' => $request->data['entityType'],
|
||||
'entityDisplayField' => $request->data['entityDisplayField'],
|
||||
'entityViewURL' => $request->data['entityViewURL'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function process($id, $requestData, $inboxRequest)
|
||||
{
|
||||
$this->discard($id, $requestData);
|
||||
return $this->genActionResult(
|
||||
[],
|
||||
true,
|
||||
__('Notification acknowledged'),
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
public function discard($id, $requestData)
|
||||
{
|
||||
return parent::discard($id, $requestData);
|
||||
}
|
||||
|
||||
private function __decodeMoreFields($request)
|
||||
{
|
||||
$decodedRequest = $request;
|
||||
$decodedRequest->data['original'] = $this->__decodeMoreChangedFields($request->data['original']);
|
||||
$decodedRequest->data['changed'] = $this->__decodeMoreChangedFields($request->data['changed']);
|
||||
return $decodedRequest;
|
||||
}
|
||||
|
||||
private function __decodeMoreChangedFields(array $fieldData): array
|
||||
{
|
||||
array_walk($fieldData, function(&$fieldValue, $fieldName) {
|
||||
if ($fieldName === 'meta_fields') {
|
||||
$fieldValue = json_decode($fieldValue, true);
|
||||
}
|
||||
});
|
||||
return $fieldData;
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ class DataExchangeProcessor extends SynchronisationInboxProcessor implements Gen
|
|||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->severity = $this->Inbox::SEVERITY_WARNING;
|
||||
$this->description = __('Handle exchange of data between two cerebrate instances');
|
||||
$this->Users = TableRegistry::getTableLocator()->get('Users');
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ class RegistrationProcessor extends UserInboxProcessor implements GenericInboxPr
|
|||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->severity = $this->Inbox::SEVERITY_WARNING;
|
||||
$this->description = __('Handle user account for this cerebrate instance');
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
if (!empty($data['summary'])) {
|
||||
$changedSummary = h($data['summary']);
|
||||
} else if (!empty($data['summaryTemplate']) && !empty($data['summaryMessage'])) {
|
||||
$changedSummary = __(
|
||||
sprintf('%s. %s.', h($data['summaryTemplate']), h($data['summaryMessage'])),
|
||||
h($data['entityType']),
|
||||
sprintf(
|
||||
'<a href="%s" target="_blank">%s</a>',
|
||||
h($data['entityViewURL']),
|
||||
h($data['entityDisplayField'])
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$changedSummary = '';
|
||||
}
|
||||
|
||||
$form = $this->element('genericElements/Form/genericForm', [
|
||||
'entity' => null,
|
||||
'ajax' => false,
|
||||
'raw' => true,
|
||||
'data' => [
|
||||
'model' => 'Inbox',
|
||||
'fields' => [],
|
||||
'submit' => [
|
||||
'action' => $this->request->getParam('action')
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
echo $this->Bootstrap->modal([
|
||||
'title' => __('Acknowledge notification'),
|
||||
'size' => 'xl',
|
||||
'type' => 'confirm',
|
||||
'bodyHtml' => sprintf(
|
||||
'<div class="d-none">%s</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<p>%s</p>
|
||||
<div class="col">%s</div>
|
||||
<div class="col">%s</div>
|
||||
</div>
|
||||
</div>',
|
||||
$form,
|
||||
$changedSummary,
|
||||
$this->Bootstrap->card([
|
||||
'headerText' => __('Original values'),
|
||||
'bodyHTML' => $this->element('genericElements/SingleViews/Fields/jsonField', ['field' => ['raw' => $data['original']]])
|
||||
]),
|
||||
$this->Bootstrap->card([
|
||||
'headerText' => __('Changed values'),
|
||||
'bodyHTML' => $this->element('genericElements/SingleViews/Fields/jsonField', ['field' => ['raw' => $data['changed']]])
|
||||
])
|
||||
),
|
||||
'confirmText' => __('Acknowledge & Discard'),
|
||||
'confirmIcon' => 'check',
|
||||
]);
|
||||
?>
|
||||
</div>
|
|
@ -70,6 +70,7 @@ class ResendFailedMessageProcessor extends BroodsOutboxProcessor implements Gene
|
|||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->description = __('Handle re-sending messages that failed to be received from other cerebrate instances.');
|
||||
$this->severity = $this->Inbox::SEVERITY_WARNING;
|
||||
$this->Broods = TableRegistry::getTableLocator()->get('Broods');
|
||||
$this->LocalTools = \Cake\ORM\TableRegistry::getTableLocator()->get('LocalTools');
|
||||
}
|
||||
|
|
|
@ -20,9 +20,14 @@ class GenericOutboxProcessor
|
|||
protected $validator;
|
||||
protected $processingTemplate = '/genericTemplates/confirm';
|
||||
protected $processingTemplatesDirectory = ROOT . '/libraries/default/OutboxProcessors/templates';
|
||||
protected $defaultSeverity;
|
||||
protected $severity;
|
||||
|
||||
|
||||
public function __construct($registerActions=false) {
|
||||
$this->Outbox = TableRegistry::getTableLocator()->get('Outbox');
|
||||
$this->Inbox = TableRegistry::getTableLocator()->get('Inbox');
|
||||
$this->defaultSeverity = $this->Inbox::SEVERITY_INFO;
|
||||
if ($registerActions) {
|
||||
$this->registerActionInProcessor();
|
||||
}
|
||||
|
@ -55,6 +60,10 @@ class GenericOutboxProcessor
|
|||
{
|
||||
return $this->description ?? '';
|
||||
}
|
||||
public function getSeverity()
|
||||
{
|
||||
return $this->severity ?? $this->defaultSeverity;
|
||||
}
|
||||
|
||||
protected function getProcessingTemplatePath()
|
||||
{
|
||||
|
@ -77,8 +86,9 @@ class GenericOutboxProcessor
|
|||
$builder = new ViewBuilder();
|
||||
$builder->disableAutoLayout()
|
||||
->setClassName('Monad')
|
||||
->setTemplate($processingTemplate);
|
||||
$view = $builder->build($viewVariables);
|
||||
->setTemplate($processingTemplate)
|
||||
->setVars($viewVariables);
|
||||
$view = $builder->build();
|
||||
$view->setRequest($serverRequest);
|
||||
return $view->render();
|
||||
}
|
||||
|
@ -193,7 +203,7 @@ class GenericOutboxProcessor
|
|||
$user_id = Router::getRequest()->getSession()->read('Auth.id');
|
||||
$requestData['scope'] = $this->scope;
|
||||
$requestData['action'] = $this->action;
|
||||
$requestData['description'] = $this->description;
|
||||
$requestData['severity'] = $this->getSeverity();
|
||||
$requestData['user_id'] = $user_id;
|
||||
$request = $this->generateRequest($requestData);
|
||||
$savedRequest = $this->Outbox->createEntry($request);
|
||||
|
|
|
@ -189,6 +189,8 @@ class TagBehavior extends Behavior
|
|||
{
|
||||
if (is_object($tag)) {
|
||||
return $tag->name;
|
||||
} else if (is_array($tag)) {
|
||||
return $tag['name'];
|
||||
} else {
|
||||
return trim($tag);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use Cake\Console\Command;
|
|||
use Cake\Console\Arguments;
|
||||
use Cake\Console\ConsoleIo;
|
||||
use Cake\Console\ConsoleOptionParser;
|
||||
use Cake\Filesystem\File;
|
||||
use Cake\Filesystem\Folder;
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\Utility\Text;
|
||||
use Cake\Validation\Validator;
|
||||
|
@ -22,7 +22,7 @@ class SummaryCommand extends Command
|
|||
{
|
||||
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
|
||||
{
|
||||
$parser->setDescription('Create a summary for data associated to the passed nationality that has been modified.');
|
||||
$parser->setDescription('Create a summary for data associated to the passed nationality that has been modified. Summaries will be printed out in STDIN and written in individual `txt` files.');
|
||||
$parser->addArgument('nationality', [
|
||||
'short' => 'n',
|
||||
'help' => 'The organisation nationality.',
|
||||
|
@ -33,6 +33,11 @@ class SummaryCommand extends Command
|
|||
'help' => 'The amount of days to look back in the logs',
|
||||
'default' => 7
|
||||
]);
|
||||
$parser->addOption('output', [
|
||||
'short' => 'o',
|
||||
'help' => 'The destination folder where to write the files',
|
||||
'default' => '/tmp'
|
||||
]);
|
||||
return $parser;
|
||||
}
|
||||
|
||||
|
@ -49,15 +54,16 @@ class SummaryCommand extends Command
|
|||
}
|
||||
foreach ($nationalities as $nationality) {
|
||||
$this->io->out(sprintf('Nationality: %s', $nationality));
|
||||
$this->_collectChangedForNationality($nationality, $days);
|
||||
$this->_collectChangedForNationality($nationality, $days, $args->getOption('output'));
|
||||
$this->io->out($io->nl(2));
|
||||
$this->io->hr();
|
||||
}
|
||||
}
|
||||
|
||||
protected function _collectChangedForNationality($nationality, $days)
|
||||
protected function _collectChangedForNationality($nationality, $days, $folderPath)
|
||||
{
|
||||
$filename = sprintf('/tmp/%s.txt', $nationality);
|
||||
$folderPath = rtrim($folderPath, '/');
|
||||
$filename = sprintf('%s/%s.txt', $folderPath, $nationality);
|
||||
$file_input = fopen($filename, 'w');
|
||||
$organisationIDsForNationality = $this->_fetchOrganisationsForNationality($nationality);
|
||||
if (empty($organisationIDsForNationality)) {
|
||||
|
|
|
@ -122,6 +122,7 @@ class AppController extends Controller
|
|||
$this->set('loggedUser', $this->ACL->getUser());
|
||||
$this->set('roleAccess', $this->ACL->getRoleAccess(false, false));
|
||||
}
|
||||
Configure::write('loggedUser', $user);
|
||||
} else if ($this->ParamHandler->isRest()) {
|
||||
throw new MethodNotAllowedException(__('Invalid user credentials.'));
|
||||
}
|
||||
|
|
|
@ -8,11 +8,10 @@ use Cake\ORM\TableRegistry;
|
|||
use \Cake\Database\Expression\QueryExpression;
|
||||
use Cake\Http\Exception\UnauthorizedException;
|
||||
use Cake\Core\Configure;
|
||||
use PhpParser\Node\Stmt\Echo_;
|
||||
|
||||
class AuditLogsController extends AppController
|
||||
{
|
||||
public $filterFields = ['model_id', 'model', 'request_action', 'user_id', 'model_title'];
|
||||
public $filterFields = ['model_id', 'model', 'request_action', 'user_id', 'model_title', 'AuditLogs.created'];
|
||||
public $quickFilterFields = ['model', 'request_action', 'model_title'];
|
||||
public $containFields = ['Users'];
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ use Cake\Utility\Inflector;
|
|||
use Cake\Utility\Text;
|
||||
use Cake\View\ViewBuilder;
|
||||
use Cake\ORM\TableRegistry;
|
||||
use Cake\ORM\Query;
|
||||
use Cake\Routing\Router;
|
||||
use Cake\Http\Exception\MethodNotAllowedException;
|
||||
use Cake\Http\Exception\NotFoundException;
|
||||
|
@ -48,6 +49,8 @@ class CRUDComponent extends Component
|
|||
$optionFilters = empty($options['filters']) ? [] : $options['filters'];
|
||||
foreach ($optionFilters as $i => $filter) {
|
||||
$optionFilters[] = "{$filter} !=";
|
||||
$optionFilters[] = "{$filter} >=";
|
||||
$optionFilters[] = "{$filter} <=";
|
||||
}
|
||||
$params = $this->Controller->ParamHandler->harvestParams($optionFilters);
|
||||
$params = $this->fakeContextFilter($options, $params);
|
||||
|
@ -112,6 +115,7 @@ class CRUDComponent extends Component
|
|||
if ($this->metaFieldsSupported()) {
|
||||
$query = $this->includeRequestedMetaFields($query);
|
||||
}
|
||||
$this->setRequestedEntryAmount();
|
||||
$data = $this->Controller->paginate($query, $this->Controller->paginate ?? []);
|
||||
if (isset($options['afterFind'])) {
|
||||
$function = $options['afterFind'];
|
||||
|
@ -195,16 +199,26 @@ class CRUDComponent extends Component
|
|||
$metaTemplates = $this->getMetaTemplates()->toArray();
|
||||
$this->Controller->set('metaFieldsEnabled', true);
|
||||
$this->Controller->set('metaTemplates', $metaTemplates);
|
||||
$typeHandlers = $this->Table->getBehavior('MetaFields')->getTypeHandlers();
|
||||
$typeHandlersOperators = [];
|
||||
foreach ($typeHandlers as $type => $handler) {
|
||||
$typeHandlersOperators[$type] = $handler::OPERATORS;
|
||||
}
|
||||
$this->Controller->set('typeHandlersOperators', $typeHandlersOperators);
|
||||
} else {
|
||||
$this->Controller->set('metaFieldsEnabled', false);
|
||||
}
|
||||
$filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : [];
|
||||
$typeHandlers = $this->Table->getBehavior('MetaFields')->getTypeHandlers();
|
||||
$typeHandlersOperators = [];
|
||||
foreach ($typeHandlers as $type => $handler) {
|
||||
$typeHandlersOperators[$type] = $handler::OPERATORS;
|
||||
}
|
||||
$this->Controller->set('typeHandlersOperators', $typeHandlersOperators);
|
||||
$typeMap = $this->Table->getSchema()->typeMap();
|
||||
$associatedtypeMap = !empty($this->Controller->filterFields) ? $this->_getAssociatedTypeMap() : [];
|
||||
$typeMap = array_merge(
|
||||
$this->Table->getSchema()->typeMap(),
|
||||
$associatedtypeMap
|
||||
);
|
||||
$typeMap = array_filter($typeMap, function ($field) use ($filters) {
|
||||
return in_array($field, $filters);
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
$this->Controller->set('typeMap', $typeMap);
|
||||
$this->Controller->set('filters', $filters);
|
||||
$this->Controller->viewBuilder()->setLayout('ajax');
|
||||
$this->Controller->render('/genericTemplates/filters');
|
||||
|
@ -466,6 +480,7 @@ class CRUDComponent extends Component
|
|||
}
|
||||
|
||||
$entity->setDirty('meta_fields', true);
|
||||
$entity->_metafields_to_delete = $metaFieldsToDelete;
|
||||
return ['entity' => $entity, 'metafields_to_delete' => $metaFieldsToDelete];
|
||||
}
|
||||
|
||||
|
@ -506,7 +521,7 @@ class CRUDComponent extends Component
|
|||
$params['contain'] = [$params['contain'], 'MetaFields'];
|
||||
}
|
||||
}
|
||||
$query = $this->Table->find()->where(['id' => $id]);
|
||||
$query = $this->Table->find()->where(["{$this->TableAlias}.id" => $id]);
|
||||
if (!empty($params['contain'])) {
|
||||
$query->contain($params['contain']);
|
||||
}
|
||||
|
@ -713,6 +728,15 @@ class CRUDComponent extends Component
|
|||
]);
|
||||
}
|
||||
|
||||
protected function setRequestedEntryAmount()
|
||||
{
|
||||
$user = $this->Controller->ACL->getUser();
|
||||
$tableSettings = IndexSetting::getTableSetting($user, $this->Table);
|
||||
if (!empty($tableSettings['number_of_element'])) {
|
||||
$this->Controller->paginate['limit'] = intval($tableSettings['number_of_element']);
|
||||
}
|
||||
}
|
||||
|
||||
public function view(int $id, array $params = []): void
|
||||
{
|
||||
if (empty($id)) {
|
||||
|
@ -1309,7 +1333,7 @@ class CRUDComponent extends Component
|
|||
{
|
||||
if (empty($params['filteringLabel']) && !empty($options['contextFilters']['custom'])) {
|
||||
foreach ($options['contextFilters']['custom'] as $contextFilter) {
|
||||
if (!empty($contextFilter['default'])) {
|
||||
if (!empty($contextFilter['default']) && empty($params)) {
|
||||
$params['filteringLabel'] = $contextFilter['label'];
|
||||
$this->Controller->set('fakeFilteringLabel', $contextFilter['label']);
|
||||
break;
|
||||
|
@ -1434,7 +1458,7 @@ class CRUDComponent extends Component
|
|||
);
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
} else if ($this->Controller->ParamHandler->isAjax()) {
|
||||
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', $message, $validationErrors);
|
||||
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', [], $message, $validationErrors);
|
||||
} else {
|
||||
$this->Controller->Flash->error($message);
|
||||
if (empty($params['redirect'])) {
|
||||
|
@ -1534,4 +1558,24 @@ class CRUDComponent extends Component
|
|||
$view = $builder->build($data);
|
||||
return $view->render();
|
||||
}
|
||||
|
||||
protected function _getAssociatedTypeMap(): array
|
||||
{
|
||||
$typeMap = [];
|
||||
foreach ($this->Controller->filterFields as $filter) {
|
||||
$exploded = explode('.', $filter);
|
||||
if (count($exploded) > 1) {
|
||||
$model = $exploded[0];
|
||||
$subField = $exploded[1];
|
||||
if ($model == $this->Table->getAlias()) {
|
||||
$typeMap[$filter] = $this->Table->getSchema()->typeMap()[$subField] ?? 'text';
|
||||
} else {
|
||||
$association = $this->Table->associations()->get($model);
|
||||
$associatedTable = $association->getTarget();
|
||||
$typeMap[$filter] = $associatedTable->getSchema()->typeMap()[$subField] ?? 'text';
|
||||
}
|
||||
}
|
||||
}
|
||||
return $typeMap;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,13 +10,13 @@ class InboxNavigation extends BaseNavigation
|
|||
$this->bcf->addRoute('Inbox', 'index', $this->bcf->defaultCRUD('Inbox', 'index'));
|
||||
$this->bcf->addRoute('Inbox', 'view', $this->bcf->defaultCRUD('Inbox', 'view'));
|
||||
$this->bcf->addRoute('Inbox', 'discard', [
|
||||
'label' => __('Discard request'),
|
||||
'label' => __('Discard message'),
|
||||
'icon' => 'trash',
|
||||
'url' => '/inbox/discard/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
]);
|
||||
$this->bcf->addRoute('Inbox', 'process', [
|
||||
'label' => __('Process request'),
|
||||
'label' => __('Process message'),
|
||||
'icon' => 'cogs',
|
||||
'url' => '/inbox/process/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
|
|
|
@ -10,13 +10,13 @@ class OutboxNavigation extends BaseNavigation
|
|||
$this->bcf->addRoute('Outbox', 'index', $this->bcf->defaultCRUD('Outbox', 'index'));
|
||||
$this->bcf->addRoute('Outbox', 'view', $this->bcf->defaultCRUD('Outbox', 'view'));
|
||||
$this->bcf->addRoute('Outbox', 'discard', [
|
||||
'label' => __('Discard request'),
|
||||
'label' => __('Discard message'),
|
||||
'icon' => 'trash',
|
||||
'url' => '/outbox/discard/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
]);
|
||||
$this->bcf->addRoute('Outbox', 'process', [
|
||||
'label' => __('Process request'),
|
||||
'label' => __('Process message'),
|
||||
'icon' => 'cogs',
|
||||
'url' => '/outbox/process/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
|
|
|
@ -16,8 +16,8 @@ use Cake\Http\Exception\ForbiddenException;
|
|||
|
||||
class InboxController extends AppController
|
||||
{
|
||||
public $filterFields = ['scope', 'action', 'title', 'origin', 'comment'];
|
||||
public $quickFilterFields = ['scope', 'action', ['title' => true], ['comment' => true]];
|
||||
public $filterFields = ['scope', 'action', 'Inbox.created', 'severity', 'title', 'origin', 'message', 'Users.id', 'Users.username',];
|
||||
public $quickFilterFields = ['scope', 'action', ['title' => true], ['message' => true], 'origin'];
|
||||
public $containFields = ['Users'];
|
||||
|
||||
public $paginate = [
|
||||
|
@ -39,9 +39,102 @@ class InboxController extends AppController
|
|||
'filters' => $this->filterFields,
|
||||
'quickFilters' => $this->quickFilterFields,
|
||||
'contextFilters' => [
|
||||
'fields' => [
|
||||
'scope',
|
||||
]
|
||||
'custom' => [
|
||||
[
|
||||
'default' => true,
|
||||
'label' => __('My Notifications'),
|
||||
'filterConditionFunction' => function ($query) {
|
||||
return $query->where(function(QueryExpression $exp) {
|
||||
return $exp->or(['user_id' => $this->ACL->getUser()['id']])
|
||||
->isNull('user_id');
|
||||
});
|
||||
}
|
||||
],
|
||||
[
|
||||
'label' => __('User Registration'),
|
||||
'filterConditionFunction' => function ($query) {
|
||||
return $query->where([
|
||||
'scope' => 'User',
|
||||
'action' => 'Registration',
|
||||
]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'label' => __('Inter-connection Requests'),
|
||||
'filterConditionFunction' => function ($query) {
|
||||
return $query->where([
|
||||
'scope' => 'LocalTool',
|
||||
'action IN' => ['IncomingConnectionRequest', 'AcceptedRequest', 'DeclinedRequest'],
|
||||
]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'label' => __('Data changed'),
|
||||
'filterConditionFunction' => function ($query) {
|
||||
return $query->where([
|
||||
'user_id' => $this->ACL->getUser()['id'], // Each admin get a message about data changes
|
||||
'scope' => 'Notification',
|
||||
'action' => 'DataChange',
|
||||
]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'label' => 'severity:primary',
|
||||
'viewElement' => 'bootstrapUI',
|
||||
'viewElementParams' => [
|
||||
'element' => 'badge',
|
||||
'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_PRIMARY],
|
||||
'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_PRIMARY],
|
||||
],
|
||||
'filterConditionFunction' => function ($query) {
|
||||
return $query->where([
|
||||
'severity' => $this->Inbox::SEVERITY_PRIMARY,
|
||||
]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'label' => 'severity:info',
|
||||
'viewElement' => 'bootstrapUI',
|
||||
'viewElementParams' => [
|
||||
'element' => 'badge',
|
||||
'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_INFO],
|
||||
'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_INFO],
|
||||
],
|
||||
'filterConditionFunction' => function ($query) {
|
||||
return $query->where([
|
||||
'severity' => $this->Inbox::SEVERITY_INFO,
|
||||
]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'label' => 'severity:warning',
|
||||
'viewElement' => 'bootstrapUI',
|
||||
'viewElementParams' => [
|
||||
'element' => 'badge',
|
||||
'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_WARNING],
|
||||
'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_WARNING],
|
||||
],
|
||||
'filterConditionFunction' => function ($query) {
|
||||
return $query->where([
|
||||
'severity' => $this->Inbox::SEVERITY_WARNING,
|
||||
]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'label' => 'severity:danger',
|
||||
'viewElement' => 'bootstrapUI',
|
||||
'viewElementParams' => [
|
||||
'element' => 'badge',
|
||||
'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_DANGER],
|
||||
'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_DANGER],
|
||||
],
|
||||
'filterConditionFunction' => function ($query) {
|
||||
return $query->where([
|
||||
'severity' => $this->Inbox::SEVERITY_DANGER,
|
||||
]);
|
||||
}
|
||||
],
|
||||
],
|
||||
],
|
||||
'contain' => $this->containFields
|
||||
]);
|
||||
|
@ -99,11 +192,11 @@ class InboxController extends AppController
|
|||
}
|
||||
}
|
||||
}
|
||||
$this->set('deletionTitle', __('Discard request'));
|
||||
$this->set('deletionTitle', __('Discard message'));
|
||||
if (!empty($id)) {
|
||||
$this->set('deletionText', __('Are you sure you want to discard request #{0}?', $id));
|
||||
$this->set('deletionText', __('Are you sure you want to discard message #{0}?', $id));
|
||||
} else {
|
||||
$this->set('deletionText', __('Are you sure you want to discard the selected requests?'));
|
||||
$this->set('deletionText', __('Are you sure you want to discard the selected message?'));
|
||||
}
|
||||
$this->set('deletionConfirm', __('Discard'));
|
||||
$this->CRUD->delete($id);
|
||||
|
|
|
@ -20,46 +20,58 @@ class OrganisationsController extends AppController
|
|||
|
||||
public function index()
|
||||
{
|
||||
$customContextFilters = [
|
||||
// [
|
||||
// 'label' => __('ENISA Accredited'),
|
||||
// 'filterCondition' => [
|
||||
// 'MetaFields.field' => 'enisa-tistatus',
|
||||
// 'MetaFields.value' => 'Accredited',
|
||||
// 'MetaFields.MetaTemplates.name' => 'ENISA CSIRT Network'
|
||||
// ]
|
||||
// ],
|
||||
// [
|
||||
// 'label' => __('ENISA not-Accredited'),
|
||||
// 'filterCondition' => [
|
||||
// 'MetaFields.field' => 'enisa-tistatus',
|
||||
// 'MetaFields.value !=' => 'Accredited',
|
||||
// 'MetaFields.MetaTemplates.name' => 'ENISA CSIRT Network'
|
||||
// ]
|
||||
// ],
|
||||
// [
|
||||
// 'label' => __('ENISA CSIRT Network (GOV)'),
|
||||
// 'filterConditionFunction' => function ($query) {
|
||||
// return $this->CRUD->setParentConditionsForMetaFields($query, [
|
||||
// 'ENISA CSIRT Network' => [
|
||||
// [
|
||||
// 'field' => 'constituency',
|
||||
// 'value LIKE' => '%Government%',
|
||||
// ],
|
||||
// [
|
||||
// 'field' => 'csirt-network-status',
|
||||
// 'value' => 'Member',
|
||||
// ],
|
||||
// ]
|
||||
// ]);
|
||||
// }
|
||||
// ],
|
||||
];
|
||||
|
||||
$loggedUserOrganisationNationality = $this->ACL->getUser()['organisation']['nationality'];
|
||||
if (!empty($loggedUserOrganisationNationality)) {
|
||||
$customContextFilters[] = [
|
||||
'label' => __('Country: {0}', $loggedUserOrganisationNationality),
|
||||
'filterCondition' => [
|
||||
'nationality' => $loggedUserOrganisationNationality,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$this->CRUD->index([
|
||||
'filters' => $this->filterFields,
|
||||
'quickFilters' => $this->quickFilterFields,
|
||||
'quickFilterForMetaField' => ['enabled' => true, 'wildcard_search' => true],
|
||||
'contextFilters' => [
|
||||
'custom' => [
|
||||
[
|
||||
'label' => __('ENISA Accredited'),
|
||||
'filterCondition' => [
|
||||
'MetaFields.field' => 'enisa-tistatus',
|
||||
'MetaFields.value' => 'Accredited',
|
||||
'MetaFields.MetaTemplates.name' => 'ENISA CSIRT Network'
|
||||
]
|
||||
],
|
||||
[
|
||||
'label' => __('ENISA not-Accredited'),
|
||||
'filterCondition' => [
|
||||
'MetaFields.field' => 'enisa-tistatus',
|
||||
'MetaFields.value !=' => 'Accredited',
|
||||
'MetaFields.MetaTemplates.name' => 'ENISA CSIRT Network'
|
||||
]
|
||||
],
|
||||
[
|
||||
'label' => __('ENISA CSIRT Network (GOV)'),
|
||||
'filterConditionFunction' => function($query) {
|
||||
return $this->CRUD->setParentConditionsForMetaFields($query, [
|
||||
'ENISA CSIRT Network' => [
|
||||
[
|
||||
'field' => 'constituency',
|
||||
'value LIKE' => '%Government%',
|
||||
],
|
||||
[
|
||||
'field' => 'csirt-network-status',
|
||||
'value' => 'Member',
|
||||
],
|
||||
]
|
||||
]);
|
||||
}
|
||||
]
|
||||
],
|
||||
'custom' => $customContextFilters,
|
||||
],
|
||||
'contain' => $this->containFields,
|
||||
'statisticsFields' => $this->statisticsFields,
|
||||
|
|
|
@ -16,8 +16,8 @@ use Cake\Http\Exception\ForbiddenException;
|
|||
|
||||
class OutboxController extends AppController
|
||||
{
|
||||
public $filterFields = ['scope', 'action', 'title', 'comment'];
|
||||
public $quickFilterFields = ['scope', 'action', ['title' => true], ['comment' => true]];
|
||||
public $filterFields = ['scope', 'action', 'title', 'message'];
|
||||
public $quickFilterFields = ['scope', 'action', ['title' => true], ['message' => true]];
|
||||
public $containFields = ['Users'];
|
||||
|
||||
public function beforeFilter(EventInterface $event)
|
||||
|
|
|
@ -21,11 +21,23 @@ class UsersController extends AppController
|
|||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
$conditions['organisation_id'] = $currentUser['organisation_id'];
|
||||
}
|
||||
$keycloakUsersParsed = null;
|
||||
if (!empty(Configure::read('keycloak.enabled'))) {
|
||||
// $keycloakUsersParsed = $this->Users->getParsedKeycloakUser();
|
||||
}
|
||||
$this->CRUD->index([
|
||||
'contain' => $this->containFields,
|
||||
'filters' => $this->filterFields,
|
||||
'quickFilters' => $this->quickFilterFields,
|
||||
'conditions' => $conditions
|
||||
'conditions' => $conditions,
|
||||
'afterFind' => function($data) use ($keycloakUsersParsed) {
|
||||
// TODO: We might want to uncomment this at some point Still need to evaluate the impact
|
||||
// if (!empty(Configure::read('keycloak.enabled'))) {
|
||||
// $keycloakUser = $keycloakUsersParsed[$data->username];
|
||||
// $data['keycloak_status'] = array_values($this->Users->checkKeycloakStatus([$data->toArray()], [$keycloakUser]))[0];
|
||||
// }
|
||||
return $data;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
|
@ -126,7 +138,7 @@ class UsersController extends AppController
|
|||
'organisation' => $this->Users->Organisations->find('list', [
|
||||
'sort' => ['name' => 'asc'],
|
||||
'conditions' => $org_conditions
|
||||
])
|
||||
])->toArray()
|
||||
];
|
||||
$this->set(compact('dropdownData'));
|
||||
$this->set('defaultRole', $defaultRole['id'] ?? null);
|
||||
|
@ -139,10 +151,18 @@ class UsersController extends AppController
|
|||
if (empty($id) || (empty($currentUser['role']['perm_org_admin']) && empty($currentUser['role']['perm_admin']))) {
|
||||
$id = $this->ACL->getUser()['id'];
|
||||
}
|
||||
$keycloakUsersParsed = null;
|
||||
if (!empty(Configure::read('keycloak.enabled'))) {
|
||||
$keycloakUsersParsed = $this->Users->getParsedKeycloakUser();
|
||||
}
|
||||
$this->CRUD->view($id, [
|
||||
'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations'],
|
||||
'afterFind' => function($data) {
|
||||
'afterFind' => function($data) use ($keycloakUsersParsed) {
|
||||
$data = $this->fetchTable('PermissionLimitations')->attachLimitations($data);
|
||||
if (!empty(Configure::read('keycloak.enabled'))) {
|
||||
$keycloakUser = $keycloakUsersParsed[$data->username];
|
||||
$data['keycloak_status'] = array_values($this->Users->checkKeycloakStatus([$data->toArray()], [$keycloakUser]))[0];
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
]);
|
||||
|
@ -150,7 +170,7 @@ class UsersController extends AppController
|
|||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('keycloakConfig', Configure::read('keycloak'));
|
||||
$this->set('keycloakConfig', Configure::read('keycloak', ['enabled' => false]));
|
||||
$this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate');
|
||||
}
|
||||
|
||||
|
@ -176,11 +196,6 @@ class UsersController extends AppController
|
|||
$id = $currentUser['id'];
|
||||
} else {
|
||||
$id = intval($id);
|
||||
if ((empty($currentUser['role']['perm_org_admin']) && empty($currentUser['role']['perm_admin']))) {
|
||||
if ($id !== $currentUser['id']) {
|
||||
throw new MethodNotAllowedException(__('You are not authorised to edit that user.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$params = [
|
||||
|
@ -189,13 +204,24 @@ class UsersController extends AppController
|
|||
],
|
||||
'fields' => [
|
||||
'password', 'confirm_password'
|
||||
]
|
||||
],
|
||||
'contain' => ['Roles', ],
|
||||
];
|
||||
if ($this->request->is(['get'])) {
|
||||
$params['fields'] = array_merge($params['fields'], ['individual_id', 'role_id', 'disabled']);
|
||||
if (!empty($this->ACL->getUser()['role']['perm_admin'])) {
|
||||
$params['fields'][] = 'organisation_id';
|
||||
}
|
||||
if (!$currentUser['role']['perm_admin']) {
|
||||
$params['afterFind'] = function ($user, &$params) use ($currentUser) {
|
||||
if (!empty($user)) { // We don't have a 404
|
||||
if (!$this->ACL->canEditUser($currentUser, $user)) {
|
||||
throw new MethodNotAllowedException(__('You cannot edit the given user.'));
|
||||
}
|
||||
}
|
||||
return $user;
|
||||
};
|
||||
}
|
||||
}
|
||||
if ($this->request->is(['post', 'put']) && !empty($this->ACL->getUser()['role']['perm_admin'])) {
|
||||
$params['fields'][] = 'individual_id';
|
||||
|
@ -210,7 +236,7 @@ class UsersController extends AppController
|
|||
if (!in_array($data['role_id'], array_keys($validRoles))) {
|
||||
throw new MethodNotAllowedException(__('You cannot edit the given privileged user.'));
|
||||
}
|
||||
if ($data['organisation_id'] !== $currentUser['organisation_id']) {
|
||||
if (!$this->ACL->canEditUser($currentUser, $data)) {
|
||||
throw new MethodNotAllowedException(__('You cannot edit the given user.'));
|
||||
}
|
||||
return $data;
|
||||
|
@ -247,7 +273,7 @@ class UsersController extends AppController
|
|||
'organisation' => $this->Users->Organisations->find('list', [
|
||||
'sort' => ['name' => 'asc'],
|
||||
'conditions' => $org_conditions
|
||||
])
|
||||
])->toArray()
|
||||
];
|
||||
$this->set(compact('dropdownData'));
|
||||
$this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate');
|
||||
|
|
|
@ -284,27 +284,34 @@ class AuthKeycloakBehavior extends Behavior
|
|||
{
|
||||
$this->updateMappers();
|
||||
$results = [];
|
||||
$data['Users'] = $this->_table->find()->contain(['Individuals', 'Organisations', 'Roles'])->select(
|
||||
[
|
||||
'id',
|
||||
'uuid',
|
||||
'username',
|
||||
'disabled',
|
||||
'Individuals.email',
|
||||
'Individuals.first_name',
|
||||
'Individuals.last_name',
|
||||
'Individuals.uuid',
|
||||
'Roles.name',
|
||||
'Roles.uuid',
|
||||
'Organisations.name',
|
||||
'Organisations.uuid'
|
||||
]
|
||||
)->disableHydration()->toArray();
|
||||
$data['Users'] = $this->getCerebrateUsers();
|
||||
$clientId = $this->getClientId();
|
||||
return $this->syncUsers($data['Users'], $clientId);
|
||||
}
|
||||
|
||||
private function syncUsers(array $users, $clientId): array
|
||||
{
|
||||
$keycloakUsersParsed = $this->getParsedKeycloakUser();
|
||||
$changes = [
|
||||
'created' => [],
|
||||
'modified' => [],
|
||||
];
|
||||
foreach ($users as &$user) {
|
||||
$changed = false;
|
||||
if (empty($keycloakUsersParsed[$user['username']])) {
|
||||
if ($this->createUser($user, $clientId)) {
|
||||
$changes['created'][] = $user['username'];
|
||||
}
|
||||
} else {
|
||||
if ($this->checkAndUpdateUser($keycloakUsersParsed[$user['username']], $user)) {
|
||||
$changes['modified'][] = $user['username'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $changes;
|
||||
}
|
||||
|
||||
public function getParsedKeycloakUser(): array
|
||||
{
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/users', [], 'get');
|
||||
$keycloakUsers = json_decode($response->getStringBody(), true);
|
||||
|
@ -325,23 +332,25 @@ class AuthKeycloakBehavior extends Behavior
|
|||
]
|
||||
];
|
||||
}
|
||||
$changes = [
|
||||
'created' => [],
|
||||
'modified' => [],
|
||||
];
|
||||
foreach ($users as &$user) {
|
||||
$changed = false;
|
||||
if (empty($keycloakUsersParsed[$user['username']])) {
|
||||
if ($this->createUser($user, $clientId)) {
|
||||
$changes['created'][] = $user['username'];
|
||||
}
|
||||
} else {
|
||||
if ($this->checkAndUpdateUser($keycloakUsersParsed[$user['username']], $user)) {
|
||||
$changes['modified'][] = $user['username'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $changes;
|
||||
return $keycloakUsersParsed;
|
||||
}
|
||||
|
||||
private function getCerebrateUsers(): array
|
||||
{
|
||||
return $this->_table->find()->contain(['Individuals', 'Organisations', 'Roles'])->select([
|
||||
'id',
|
||||
'uuid',
|
||||
'username',
|
||||
'disabled',
|
||||
'Individuals.email',
|
||||
'Individuals.first_name',
|
||||
'Individuals.last_name',
|
||||
'Individuals.uuid',
|
||||
'Roles.name',
|
||||
'Roles.uuid',
|
||||
'Organisations.name',
|
||||
'Organisations.uuid'
|
||||
])->disableHydration()->toArray();
|
||||
}
|
||||
|
||||
private function checkAndUpdateUser(array $keycloakUser, array $user): bool
|
||||
|
@ -387,6 +396,63 @@ class AuthKeycloakBehavior extends Behavior
|
|||
return false;
|
||||
}
|
||||
|
||||
public function checkKeycloakStatus(array $users, array $keycloakUsers): array
|
||||
{
|
||||
$users = Hash::combine($users, '{n}.username', '{n}');
|
||||
$keycloakUsersParsed = Hash::combine($keycloakUsers, '{n}.username', '{n}');
|
||||
$status = [];
|
||||
foreach ($users as $username => $user) {
|
||||
$differences = [];
|
||||
$requireUpdate = $this->checkKeycloakUserRequiresUpdate($keycloakUsersParsed[$username], $user, $differences);
|
||||
$status[$user['id']] = [
|
||||
'require_update' => $requireUpdate,
|
||||
'differences' => $differences,
|
||||
];
|
||||
}
|
||||
return $status;
|
||||
}
|
||||
|
||||
private function checkKeycloakUserRequiresUpdate(array $keycloakUser, array $user, array &$differences = []): bool
|
||||
{
|
||||
|
||||
$cEnabled = $keycloakUser['enabled'] == $user['disabled'];
|
||||
$cFn = $keycloakUser['firstName'] !== $user['individual']['first_name'];
|
||||
$Ln = $keycloakUser['lastName'] !== $user['individual']['last_name'];
|
||||
$cEmail = $keycloakUser['email'] !== $user['individual']['email'];
|
||||
$cRolename = (empty($keycloakUser['attributes']['role_name']) || $keycloakUser['attributes']['role_name'] !== $user['role']['name']);
|
||||
$cRoleuuid = (empty($keycloakUser['attributes']['role_uuid']) || $keycloakUser['attributes']['role_uuid'] !== $user['role']['uuid']);
|
||||
$cOrgname = (empty($keycloakUser['attributes']['org_name']) || $keycloakUser['attributes']['org_name'] !== $user['organisation']['name']);
|
||||
$cOrguuid = (empty($keycloakUser['attributes']['org_uuid']) || $keycloakUser['attributes']['org_uuid'] !== $user['organisation']['uuid']);
|
||||
if ($cEnabled || $cFn || $Ln || $cEmail || $cRolename || $cRoleuuid || $cOrgname || $cOrguuid) {
|
||||
if ($cEnabled) {
|
||||
$differences['enabled'] = ['kc' => $keycloakUser['enabled'], 'cerebrate' => $user['disabled']];
|
||||
}
|
||||
if ($cFn) {
|
||||
$differences['first_name'] = ['kc' => $keycloakUser['firstName'], 'cerebrate' => $user['individual']['first_name']];
|
||||
}
|
||||
if ($Ln) {
|
||||
$differences['last_name'] = ['kc' => $keycloakUser['lastName'], 'cerebrate' => $user['individual']['last_name']];
|
||||
}
|
||||
if ($cEmail) {
|
||||
$differences['email'] = ['kc' => $keycloakUser['email'], 'cerebrate' => $user['individual']['email']];
|
||||
}
|
||||
if ($cRolename) {
|
||||
$differences['role_name'] = ['kc' => $keycloakUser['attributes']['role_name'], 'cerebrate' => $user['role']['name']];
|
||||
}
|
||||
if ($cRoleuuid) {
|
||||
$differences['role_uuid'] = ['kc' => $keycloakUser['attributes']['role_uuid'], 'cerebrate' => $user['role']['uuid']];
|
||||
}
|
||||
if ($cOrgname) {
|
||||
$differences['org_name'] = ['kc' => $keycloakUser['attributes']['org_name'], 'cerebrate' => $user['organisation']['name']];
|
||||
}
|
||||
if ($cOrguuid) {
|
||||
$differences['org_uuid'] = ['kc' => $keycloakUser['attributes']['org_uuid'], 'cerebrate' => $user['organisation']['uuid']];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function createUser(array $user, string $clientId)
|
||||
{
|
||||
$newUser = [
|
||||
|
|
|
@ -0,0 +1,332 @@
|
|||
<?php
|
||||
namespace App\Model\Behavior;
|
||||
|
||||
use ArrayObject;
|
||||
use App\Model\Entity\AppModel;
|
||||
use App\Model\Entity\MetaField;
|
||||
use Cake\Core\Configure;
|
||||
use App\Model\Table\UsersTable;
|
||||
use Cake\ORM\Behavior;
|
||||
use Cake\Datasource\EntityInterface;
|
||||
use Cake\Event\EventInterface;
|
||||
use Cake\ORM\TableRegistry;
|
||||
use Cake\ORM\Query;
|
||||
use Cake\Utility\Inflector;
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\Routing\Router;
|
||||
|
||||
class NotifyAdminsBehavior extends Behavior
|
||||
{
|
||||
/** @var array */
|
||||
protected $_defaultConfig = [
|
||||
'implementedEvents' => [
|
||||
'Model.afterSave' => 'afterSave',
|
||||
'Model.afterDelete' => 'afterDelete',
|
||||
'Model.beforeDelete' => 'beforeDelete',
|
||||
],
|
||||
'implementedMethods' => [
|
||||
'notifySiteAdmins' => 'notifySiteAdmins',
|
||||
'notifySiteAdminsForEntity' => 'notifySiteAdminsForEntity',
|
||||
],
|
||||
];
|
||||
|
||||
/** @var AuditLog|null */
|
||||
private $Inbox;
|
||||
/** @var User|null */
|
||||
private $Users;
|
||||
/** @var InboxProcessors|null */
|
||||
private $InboxProcessors;
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
$this->Inbox = TableRegistry::getTableLocator()->get('Inbox');
|
||||
if($this->table() instanceof UsersTable) {
|
||||
$this->Users = $this->table();
|
||||
} else {
|
||||
$this->Users = TableRegistry::getTableLocator()->get('Users');
|
||||
}
|
||||
}
|
||||
|
||||
public function afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
|
||||
{
|
||||
if (!$this->isNotificationAllowed($event, $entity, $options)) {
|
||||
return;
|
||||
}
|
||||
$this->notifySiteAdminsForEntityChange($entity);
|
||||
}
|
||||
|
||||
public function beforeDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
|
||||
{
|
||||
if ($entity->table()->hasBehavior('MetaFields') && !isset($entity->meta_fields)) {
|
||||
$entity = $entity->table()->loadInto($entity, ['MetaFields']);
|
||||
}
|
||||
}
|
||||
|
||||
public function afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
|
||||
{
|
||||
if (!$this->isNotificationAllowed($event, $entity, $options)) {
|
||||
return;
|
||||
}
|
||||
$this->notifySiteAdminsForEntityDeletion($entity);
|
||||
}
|
||||
|
||||
public function isNotificationAllowed(EventInterface $event, EntityInterface $entity, ArrayObject $options): bool
|
||||
{
|
||||
$loggedUser = Configure::read('loggedUser');
|
||||
if (empty($loggedUser) || !empty($loggedUser['role']['perm_admin']) || !empty($loggedUser['role']['perm_sync'])) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function notifySiteAdminsForEntityChange(EntityInterface $entity): void
|
||||
{
|
||||
$watchedFields = !empty($this->getConfig()['fields']) ? $this->getConfig()['fields'] : $entity->getVisible();
|
||||
$originalFields = [];
|
||||
$changedFields = $entity->extract($watchedFields);
|
||||
|
||||
$titleTemplate = 'New {0} `{1}` created';
|
||||
$title = __(
|
||||
$titleTemplate,
|
||||
Inflector::singularize($entity->getSource()),
|
||||
$entity->get($this->table()->getDisplayField())
|
||||
);
|
||||
$message = __('New {0}', Inflector::singularize($entity->getSource()));
|
||||
|
||||
if (!$entity->isNew()) {
|
||||
$originalFields = $this->_getOriginalFields($entity, $watchedFields);
|
||||
$changedFields = $this->_getChangedFields($entity, $originalFields);
|
||||
|
||||
if (
|
||||
$entity->table()->hasBehavior('Timestamp') &&
|
||||
count($changedFields) == 1 && !empty($changedFields['modified'])
|
||||
) {
|
||||
return; // A not watched field has changed
|
||||
}
|
||||
|
||||
$changeAmount = $this->_computeChangeAmount($entity, $originalFields, $changedFields);
|
||||
$titleTemplate = '{0} {1} modified';
|
||||
$title = __(
|
||||
$titleTemplate,
|
||||
Inflector::singularize($entity->getSource()),
|
||||
$entity->get($this->table()->getDisplayField())
|
||||
);
|
||||
$message = __n(
|
||||
'{0} field was updated',
|
||||
'{0} fields were updated',
|
||||
$changeAmount,
|
||||
$changeAmount
|
||||
);
|
||||
}
|
||||
if ($entity->table()->hasBehavior('MetaFields')) {
|
||||
$originalFields['meta_fields'] = $this->_massageMetaField($entity, $originalFields['meta_fields'] ?? []);
|
||||
$changedFields['meta_fields'] = $this->_massageMetaField($entity, $changedFields['meta_fields'] ?? []);
|
||||
if (empty($originalFields['meta_fields']) && empty($changedFields['meta_fields'])) {
|
||||
unset($originalFields['meta_fields']);
|
||||
unset($changedFields['meta_fields']);
|
||||
}
|
||||
}
|
||||
$originalFields = $entity->isNew() ? [] : $originalFields;
|
||||
$data = [
|
||||
'original' => $this->_serializeFields($originalFields),
|
||||
'changed' => $this->_serializeFields($changedFields),
|
||||
'summaryTemplate' => $titleTemplate,
|
||||
'summaryMessage' => $message,
|
||||
'entityType' => Inflector::singularize($entity->getSource()),
|
||||
'entityDisplayField' => $entity->get($this->table()->getDisplayField()),
|
||||
'entityViewURL' => Router::url([
|
||||
'controller' => Inflector::underscore($entity->getSource()),
|
||||
'action' => 'view',
|
||||
$entity->id
|
||||
]),
|
||||
];
|
||||
$this->notifySiteAdmins($entity->getSource(), $title, $message, $data);
|
||||
}
|
||||
|
||||
public function notifySiteAdminsForEntityDeletion(EntityInterface $entity): void
|
||||
{
|
||||
$watchedFields = !empty($this->getConfig()['fields']) ? $this->getConfig()['fields'] : $entity->getVisible();
|
||||
$originalFields = $entity->extract($watchedFields);
|
||||
$changedFields = [];
|
||||
$titleTemplate = 'Deleted {0} `{1}`';
|
||||
$title = __(
|
||||
$titleTemplate,
|
||||
Inflector::singularize($entity->getSource()),
|
||||
$entity->get($this->table()->getDisplayField())
|
||||
);
|
||||
$message = __('{0} deleted', Inflector::singularize($entity->getSource()));
|
||||
$data = [
|
||||
'original' => $this->_serializeFields($originalFields),
|
||||
'changed' => $this->_serializeFields($changedFields),
|
||||
'summaryTemplate' => $titleTemplate,
|
||||
'summaryMessage' => $message,
|
||||
'entityType' => Inflector::singularize($entity->getSource()),
|
||||
'entityDisplayField' => $entity->get($this->table()->getDisplayField()),
|
||||
'entityViewURL' => Router::url([
|
||||
'controller' => Inflector::underscore($entity->getSource()),
|
||||
'action' => 'view',
|
||||
$entity->id
|
||||
]),
|
||||
];
|
||||
$this->notifySiteAdmins($entity->getSource(), $title, $message, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a message in each site-admin users
|
||||
*
|
||||
* @param string $title A quick summary of what that notification is about
|
||||
* @param string $origin The origin of the notification. For example, could be the model source
|
||||
* @param string $message Even more free text
|
||||
* @param array $data data used to generate the view when processing.
|
||||
* Must contain: `original` array of the data before the change
|
||||
* Must contain: `changed` array of the data after the change
|
||||
* Optional: `summary` A text summarizing the change
|
||||
* Optional: `summaryTemplate`, `summaryMessage`, `entityType`, `entityDisplayField`, `entityViewURL` text used to build a summary of the change
|
||||
* @return void
|
||||
*/
|
||||
public function notifySiteAdmins(
|
||||
string $origin,
|
||||
string $title,
|
||||
string $message = '',
|
||||
array $data
|
||||
): void {
|
||||
$this->InboxProcessors = $this->InboxProcessors ?: TableRegistry::getTableLocator()->get('InboxProcessors');
|
||||
$processor = $this->InboxProcessors->getProcessor('Notification', 'DataChange');
|
||||
$siteAdmins = $this->_getSiteAdmins();
|
||||
foreach ($siteAdmins as $siteAdmin) {
|
||||
$notificationData = [
|
||||
'origin' => $origin,
|
||||
'title' => $title,
|
||||
'user_id' => $siteAdmin->id,
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
];
|
||||
$processor->create($notificationData);
|
||||
}
|
||||
}
|
||||
|
||||
protected function _getSiteAdmins(): array
|
||||
{
|
||||
return $this->Users->find()
|
||||
->matching('Roles', function(Query $q) {
|
||||
return $q
|
||||
->where(['Roles.perm_admin' => true]);
|
||||
})
|
||||
->all()->toList();
|
||||
}
|
||||
|
||||
protected function _getOriginalFields(AppModel $entity, array $fields): array
|
||||
{
|
||||
$originalChangedFields = $entity->extractOriginalChanged($fields);
|
||||
$originalChangedFields = array_map(function ($fieldValue) {
|
||||
if (is_array($fieldValue)) {
|
||||
return array_filter(array_map(function($fieldValue) {
|
||||
if (is_subclass_of($fieldValue, 'App\Model\Entity\AppModel')) {
|
||||
if (!empty($fieldValue->extractOriginalChanged($fieldValue->getVisible()))) {
|
||||
return $fieldValue;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return $fieldValue;
|
||||
}, $fieldValue), fn ($v) => !is_null($v));
|
||||
} else if (is_subclass_of($fieldValue, 'App\Model\Entity\AppModel')) {
|
||||
return $fieldValue->extractOriginalChanged($fieldValue);
|
||||
}
|
||||
return $fieldValue;
|
||||
}, $originalChangedFields);
|
||||
if ($entity->table()->hasBehavior('MetaFields')) {
|
||||
// Include deleted meta-fields
|
||||
$originalChangedFields['meta_fields'] = array_merge(
|
||||
$originalChangedFields['meta_fields'] ?? [],
|
||||
$this->_getDeletedMetafields($entity)
|
||||
);
|
||||
// Restore original values of meta-fields as the entity has been saved with the changes
|
||||
if (!empty($entity->meta_fields)) {
|
||||
$originalChangedFields['meta_fields'] = array_map(function ($metaField) {
|
||||
$originalValues = $metaField->getOriginalValues($metaField->getVisible());
|
||||
$originalMetafield = $metaField->table()->newEntity($metaField->toArray());
|
||||
$originalMetafield->set($originalValues);
|
||||
return $originalMetafield;
|
||||
}, $originalChangedFields['meta_fields']);
|
||||
}
|
||||
}
|
||||
return $originalChangedFields;
|
||||
}
|
||||
|
||||
protected function _getChangedFields(AppModel $entity, array $originalFields): array
|
||||
{
|
||||
$changedFields = array_filter(
|
||||
$entity->extract(array_keys($originalFields)),
|
||||
fn ($v) => !is_null($v)
|
||||
);
|
||||
if ($entity->table()->hasBehavior('MetaFields')) {
|
||||
$changedMetafields = $entity->extractOriginalChanged($entity->getVisible())['meta_fields'] ?? [];
|
||||
$changedFields['meta_fields'] = array_filter($changedMetafields, function($metaField) {
|
||||
return !empty($metaField->getDirty());
|
||||
});
|
||||
}
|
||||
return $changedFields;
|
||||
}
|
||||
|
||||
protected function _massageMetaField(AppModel $entity, array $metaFields): array
|
||||
{
|
||||
$massaged = [];
|
||||
foreach ($metaFields as $metaField) {
|
||||
foreach ($entity->MetaTemplates as $template) {
|
||||
$templateDisplayName = sprintf('%s (v%s)', $template->name, $template->version);
|
||||
foreach ($template['meta_template_fields'] as $field) {
|
||||
if ($metaField->meta_template_id == $template->id && $metaField->meta_template_field_id == $field->id) {
|
||||
if (!empty($massaged[$templateDisplayName][$field['field']])) {
|
||||
if (!is_array($massaged[$templateDisplayName][$field['field']])) {
|
||||
$massaged[$templateDisplayName][$field['field']] = [$massaged[$templateDisplayName][$field['field']]];
|
||||
}
|
||||
$massaged[$templateDisplayName][$field['field']][] = $metaField['value'];
|
||||
} else {
|
||||
$massaged[$templateDisplayName][$field['field']] = $metaField['value'];
|
||||
}
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $massaged;
|
||||
}
|
||||
|
||||
protected function _getDeletedMetafields(AppModel $entity): array
|
||||
{
|
||||
return $entity->_metafields_to_delete ?? [];
|
||||
}
|
||||
|
||||
protected function _computeChangeAmount(AppModel $entity, array $originalFields, array $changedFields): int
|
||||
{
|
||||
$amount = count($changedFields);
|
||||
if ($entity->table()->hasBehavior('MetaFields')) {
|
||||
$amount -= 1; // `meta_fields` key was counted without checking at the content
|
||||
}
|
||||
if (!empty($originalFields['meta_fields']) && !empty($changedFields['meta_fields'])) {
|
||||
$amount += count(array_intersect_key($originalFields['meta_fields'] ?? [], $changedFields['meta_fields'] ?? [])); // Add changed fields
|
||||
$amount += count(array_diff_key($changedFields['meta_fields'] ?? [], $originalFields['meta_fields'] ?? [])); // Add new fields
|
||||
$amount += count(array_diff_key($originalFields['meta_fields'] ?? [], $changedFields['meta_fields'] ?? [])); // Add deleted fields
|
||||
}
|
||||
return $amount;
|
||||
}
|
||||
|
||||
protected function _serializeFields($fields): array
|
||||
{
|
||||
return array_map(function ($fieldValue) {
|
||||
if (is_bool($fieldValue)) {
|
||||
return empty($fieldValue) ? 0 : 1;
|
||||
} else if (is_array($fieldValue)) {
|
||||
return json_encode($fieldValue);
|
||||
} else if (is_object($fieldValue)) {
|
||||
switch (get_class($fieldValue)) {
|
||||
case 'Cake\I18n\FrozenTime':
|
||||
return $fieldValue->i18nFormat('yyyy-mm-dd HH:mm:ss');
|
||||
}
|
||||
} else {
|
||||
return strval($fieldValue);
|
||||
}
|
||||
}, $fields);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
namespace App\Model\Entity;
|
||||
|
||||
use App\Model\Table\AppTable;
|
||||
use Cake\ORM\Entity;
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
class AppModel extends Entity
|
||||
{
|
||||
|
@ -33,6 +35,11 @@ class AppModel extends Entity
|
|||
return $this->_accessibleOnNew ?? [];
|
||||
}
|
||||
|
||||
public function table(): AppTable
|
||||
{
|
||||
return TableRegistry::get($this->getSource());
|
||||
}
|
||||
|
||||
public function rearrangeForAPI(): void
|
||||
{
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use Cake\ORM\Entity;
|
|||
|
||||
class Inbox extends AppModel
|
||||
{
|
||||
protected $_virtual = ['local_tool_connector_name'];
|
||||
protected $_virtual = ['local_tool_connector_name', 'severity_variant'];
|
||||
|
||||
protected function _getLocalToolConnectorName()
|
||||
{
|
||||
|
@ -17,4 +17,9 @@ class Inbox extends AppModel
|
|||
}
|
||||
return $localConnectorName;
|
||||
}
|
||||
|
||||
protected function _getSeverityVariant(): string
|
||||
{
|
||||
return $this->table()->severityVariant[$this->severity];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,5 +7,45 @@ use Cake\ORM\Entity;
|
|||
|
||||
class MetaTemplateField extends AppModel
|
||||
{
|
||||
protected $_virtual = ['index_type', 'form_type', 'form_options', ];
|
||||
|
||||
protected function _getIndexType()
|
||||
{
|
||||
$indexType = 'text';
|
||||
if ($this->type === 'boolean') {
|
||||
$indexType = 'boolean';
|
||||
} else if ($this->type === 'date') {
|
||||
$indexType = 'datetime';
|
||||
} else if ($this->type === 'ipv4' || $this->type === 'ipv6') {
|
||||
$indexType = 'text';
|
||||
}
|
||||
return $indexType;
|
||||
}
|
||||
|
||||
protected function _getFormType()
|
||||
{
|
||||
$formType = 'text';
|
||||
if (!empty($this->sane_default) || !empty($this->values_list)) {
|
||||
$formType = 'dropdown';
|
||||
} else if ($this->type === 'boolean') {
|
||||
$formType = 'checkbox';
|
||||
}
|
||||
return $formType;
|
||||
}
|
||||
|
||||
protected function _getFormOptions()
|
||||
{
|
||||
$formOptions = [];
|
||||
if ($this->formType === 'dropdown') {
|
||||
$selectOptions = !empty($this->sane_default) ? $this->sane_default : $this->values_list;
|
||||
$selectOptions = array_combine($selectOptions, $selectOptions);
|
||||
if (!empty($this->sane_default)) {
|
||||
$selectOptions[] = ['value' => '_custom', 'text' => __('-- custom value --'), 'class' => 'custom-value'];
|
||||
}
|
||||
$selectOptions[''] = __('-- no value --');
|
||||
$formOptions['options'] = $selectOptions;
|
||||
}
|
||||
return $formOptions;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,5 +7,10 @@ use Cake\ORM\Entity;
|
|||
|
||||
class Outbox extends AppModel
|
||||
{
|
||||
|
||||
protected $_virtual = ['severity_variant'];
|
||||
|
||||
protected function _getSeverityVariant(): string
|
||||
{
|
||||
return $this->table()->severityVariant[$this->severity];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ class AppTable extends Table
|
|||
{
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
FrozenTime::setToStringFormat("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
public function getStatisticsUsageForModel(Object $table, array $scopes, array $options=[]): array
|
||||
|
|
|
@ -8,6 +8,7 @@ use Cake\ORM\Table;
|
|||
use Cake\ORM\RulesChecker;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\Http\Exception\NotFoundException;
|
||||
use Cake\ORM\ResultSet;
|
||||
|
||||
use App\Utility\UI\Notification;
|
||||
|
||||
|
@ -15,6 +16,17 @@ Type::map('json', 'Cake\Database\Type\JsonType');
|
|||
|
||||
class InboxTable extends AppTable
|
||||
{
|
||||
public const SEVERITY_PRIMARY = 0,
|
||||
SEVERITY_INFO = 1,
|
||||
SEVERITY_WARNING = 2,
|
||||
SEVERITY_DANGER = 3;
|
||||
|
||||
public $severityVariant = [
|
||||
self::SEVERITY_PRIMARY => 'primary',
|
||||
self::SEVERITY_INFO => 'info',
|
||||
self::SEVERITY_WARNING => 'warning',
|
||||
self::SEVERITY_DANGER => 'danger',
|
||||
];
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
|
@ -97,8 +109,8 @@ class InboxTable extends AppTable
|
|||
$allNotifications = [];
|
||||
$inboxNotifications = $this->getNotificationsForUser($user);
|
||||
foreach ($inboxNotifications as $notification) {
|
||||
$title = __('New message');
|
||||
$details = $notification->title;
|
||||
$title = $notification->title;
|
||||
$details = $notification->message;
|
||||
$router = [
|
||||
'controller' => 'inbox',
|
||||
'action' => 'process',
|
||||
|
@ -109,7 +121,7 @@ class InboxTable extends AppTable
|
|||
'icon' => 'envelope',
|
||||
'details' => $details,
|
||||
'datetime' => $notification->created,
|
||||
'variant' => 'warning',
|
||||
'variant' => $notification->severity_variant,
|
||||
'_useModal' => true,
|
||||
'_sidebarId' => 'inbox',
|
||||
]))->get();
|
||||
|
@ -117,18 +129,14 @@ class InboxTable extends AppTable
|
|||
return $allNotifications;
|
||||
}
|
||||
|
||||
public function getNotificationsForUser(\App\Model\Entity\User $user): array
|
||||
public function getNotificationsForUser(\App\Model\Entity\User $user): ResultSet
|
||||
{
|
||||
$query = $this->find();
|
||||
$conditions = [];
|
||||
if ($user['role']['perm_admin']) {
|
||||
// Admin will not see notifications if it doesn't belong to them. They can see process the message from the inbox
|
||||
$conditions['Inbox.user_id IS'] = null;
|
||||
} else {
|
||||
$conditions['Inbox.user_id'] = $user->id;
|
||||
}
|
||||
$conditions = [
|
||||
'Inbox.user_id' => $user->id
|
||||
];
|
||||
$query->where($conditions);
|
||||
$notifications = $query->all()->toArray();
|
||||
$notifications = $query->all();
|
||||
return $notifications;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ class MetaTemplateFieldsTable extends AppTable
|
|||
$this->hasMany('MetaFields');
|
||||
|
||||
$this->setDisplayField('field');
|
||||
$this->getSchema()->setColumnType('sane_default', 'json');
|
||||
$this->getSchema()->setColumnType('values_list', 'json');
|
||||
$this->loadTypeHandlers();
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,9 @@ class OrganisationsTable extends AppTable
|
|||
$this->addBehavior('Timestamp');
|
||||
$this->addBehavior('Tags.Tag');
|
||||
$this->addBehavior('AuditLog');
|
||||
$this->addBehavior('NotifyAdmins', [
|
||||
'fields' => ['uuid', 'name', 'url', 'nationality', 'sector', 'type', 'contacts', 'modified', 'meta_fields'],
|
||||
]);
|
||||
$this->hasMany(
|
||||
'Alignments',
|
||||
[
|
||||
|
|
|
@ -8,11 +8,13 @@ use Cake\ORM\Table;
|
|||
use Cake\ORM\RulesChecker;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\Http\Exception\NotFoundException;
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
Type::map('json', 'Cake\Database\Type\JsonType');
|
||||
|
||||
class OutboxTable extends AppTable
|
||||
{
|
||||
public $severityVariant;
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
|
@ -22,6 +24,9 @@ class OutboxTable extends AppTable
|
|||
$this->belongsTo('Users');
|
||||
$this->addBehavior('AuditLog');
|
||||
$this->setDisplayField('title');
|
||||
|
||||
$this->Inbox = TableRegistry::getTableLocator()->get('Inbox');
|
||||
$this->severityVariant = $this->Inbox->severityVariant;
|
||||
}
|
||||
|
||||
protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaInterface
|
||||
|
|
|
@ -22,6 +22,7 @@ class PermissionLimitationsTable extends AppTable
|
|||
$validator
|
||||
->notEmptyString('permission')
|
||||
->notEmptyString('scope')
|
||||
->naturalNumber('max_occurrence', __('That field can only hold non-negative values.'))
|
||||
->requirePresence(['permission', 'scope', 'max_occurrence'], 'create');
|
||||
return $validator;
|
||||
}
|
||||
|
@ -44,6 +45,14 @@ class PermissionLimitationsTable extends AppTable
|
|||
'limit' => $entry['max_occurrence']
|
||||
];
|
||||
}
|
||||
foreach ($limitations as $i => $permissions) { // Make sure global and organisations permission are mirror in the case where one of the two is not defined
|
||||
if (!isset($permissions['global']['limit'])) {
|
||||
$limitations[$i]['global']['limit'] = $permissions['organisation']['limit'];
|
||||
}
|
||||
if (!isset($permissions['organisation']['limit'])) {
|
||||
$limitations[$i]['organisation']['limit'] = $permissions['global']['limit'];
|
||||
}
|
||||
}
|
||||
foreach ($limitations as $field => $data) {
|
||||
if (isset($data['global'])) {
|
||||
$limitations[$field]['global']['current'] = $MetaFields->find('all', [
|
||||
|
|
|
@ -25,6 +25,9 @@ class UsersTable extends AppTable
|
|||
$this->addBehavior('UUID');
|
||||
$this->addBehavior('MetaFields');
|
||||
$this->addBehavior('AuditLog');
|
||||
$this->addBehavior('NotifyAdmins', [
|
||||
'fields' => ['role_id', 'individual_id', 'organisation_id', 'disabled', 'modified', 'meta_fields'],
|
||||
]);
|
||||
$this->initAuthBehaviors();
|
||||
$this->belongsTo(
|
||||
'Individuals',
|
||||
|
@ -66,6 +69,7 @@ class UsersTable extends AppTable
|
|||
|
||||
public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
|
||||
{
|
||||
$success = true;
|
||||
if (!$entity->isNew()) {
|
||||
$success = $this->handleUserUpdateRouter($entity);
|
||||
}
|
||||
|
@ -84,22 +88,33 @@ class UsersTable extends AppTable
|
|||
if (!isset($this->PermissionLimitations)) {
|
||||
$this->PermissionLimitations = TableRegistry::get('PermissionLimitations');
|
||||
}
|
||||
$new = $entity->isNew();
|
||||
$permissions = $this->PermissionLimitations->getListOfLimitations($entity);
|
||||
foreach ($permissions as $permission_name => $permission) {
|
||||
foreach ($permission as $scope => $permission_data) {
|
||||
if (!empty($entity['meta_fields'])) {
|
||||
$enabled = false;
|
||||
$valueToCompareTo = $permission_data['current'];
|
||||
|
||||
$enabled = false;
|
||||
if (!empty($entity->meta_fields)) {
|
||||
foreach ($entity['meta_fields'] as $metaField) {
|
||||
if ($metaField['field'] === $permission_name) {
|
||||
$enabled = true;
|
||||
if ($metaField->isNew()) {
|
||||
$valueToCompareTo += !empty($metaField->value) ? 1 : 0;
|
||||
} else {
|
||||
$valueToCompareTo += !empty($metaField->value) ? 0 : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$enabled && !empty($entity->_metafields_to_delete)) {
|
||||
foreach ($entity->_metafields_to_delete as $metaFieldToDelete) {
|
||||
if ($metaFieldToDelete['field'] === $permission_name) {
|
||||
$valueToCompareTo += !empty($metaFieldToDelete->value) ? -1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
$valueToCompareTo = $permission_data['current'] + ($new ? 1 : 0);
|
||||
|
||||
if ($valueToCompareTo > $permission_data['limit']) {
|
||||
return [
|
||||
$permission_name =>
|
||||
|
@ -168,18 +183,6 @@ class UsersTable extends AppTable
|
|||
return $rules;
|
||||
}
|
||||
|
||||
public function test()
|
||||
{
|
||||
$this->Roles = TableRegistry::get('Roles');
|
||||
$role = $this->Roles->newEntity([
|
||||
'name' => 'admin',
|
||||
'perm_admin' => 1,
|
||||
'perm_org_admin' => 1,
|
||||
'perm_sync' => 1
|
||||
]);
|
||||
$this->Roles->save($role);
|
||||
}
|
||||
|
||||
public function checkForNewInstance(): bool
|
||||
{
|
||||
if (empty($this->find()->first())) {
|
||||
|
@ -263,7 +266,7 @@ class UsersTable extends AppTable
|
|||
{
|
||||
if (!empty(Configure::read('keycloak'))) {
|
||||
$success = $this->handleUserUpdate($user);
|
||||
//return $success !== false;
|
||||
// return $success;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ class Notification
|
|||
foreach ($options as $key => $value) {
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
$this->validate();
|
||||
$this->_validate();
|
||||
}
|
||||
|
||||
public function get(): array
|
||||
|
@ -36,7 +36,7 @@ class Notification
|
|||
return null;
|
||||
}
|
||||
|
||||
private function validate()
|
||||
protected function _validate()
|
||||
{
|
||||
$validator = new Validator();
|
||||
|
||||
|
|
|
@ -1077,6 +1077,7 @@ class BoostrapModal extends BootstrapGeneric
|
|||
'bodyHtml' => false,
|
||||
'footerHtml' => false,
|
||||
'confirmText' => 'Confirm',
|
||||
'confirmIcon' => false,
|
||||
'cancelText' => 'Cancel',
|
||||
'modalClass' => [''],
|
||||
'headerClass' => [''],
|
||||
|
@ -1218,6 +1219,7 @@ class BoostrapModal extends BootstrapGeneric
|
|||
$buttonConfirm = (new BoostrapButton([
|
||||
'variant' => $variant,
|
||||
'text' => h($this->options['confirmText']),
|
||||
'icon' => h($this->options['confirmIcon']),
|
||||
'class' => 'modal-confirm-button',
|
||||
'params' => [
|
||||
// 'data-bs-dismiss' => $this->options['confirmFunction'] ? '' : 'modal',
|
||||
|
@ -1953,7 +1955,7 @@ class BoostrapDropdownMenu extends BootstrapGeneric
|
|||
'aria-expanded' => 'false',
|
||||
]
|
||||
];
|
||||
$options = array_merge($this->options['toggle-button'], $defaultOptions);
|
||||
$options = array_merge_recursive($this->options['toggle-button'], $defaultOptions);
|
||||
return $this->btHelper->button($options);
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,12 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'sort' => 'request_ip',
|
||||
'data_path' => 'request_ip',
|
||||
],
|
||||
[
|
||||
'name' => 'created',
|
||||
'sort' => 'created',
|
||||
'data_path' => 'created',
|
||||
'element' => 'datetime'
|
||||
],
|
||||
[
|
||||
'name' => __('Username'),
|
||||
'sort' => 'user.username',
|
||||
|
|
|
@ -12,9 +12,9 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'type' => 'multi_select_actions',
|
||||
'children' => [
|
||||
[
|
||||
'text' => __('Discard requests'),
|
||||
'text' => __('Discard message'),
|
||||
'variant' => 'danger',
|
||||
'onclick' => 'discardRequests',
|
||||
'onclick' => 'discardMessages',
|
||||
]
|
||||
],
|
||||
'data' => [
|
||||
|
@ -34,6 +34,10 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
'allowFilering' => true
|
||||
],
|
||||
[
|
||||
'type' => 'table_action',
|
||||
'table_setting_id' => 'inbox_index',
|
||||
]
|
||||
]
|
||||
],
|
||||
|
@ -49,6 +53,18 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'data_path' => 'created',
|
||||
'element' => 'datetime'
|
||||
],
|
||||
[
|
||||
'name' => 'severity',
|
||||
'sort' => 'severity',
|
||||
'data_path' => 'severity',
|
||||
'element' => 'function',
|
||||
'function' => function ($entry, $context) {
|
||||
return $context->Bootstrap->badge([
|
||||
'text' => $entry->severity_variant,
|
||||
'variant' => $entry->severity_variant,
|
||||
]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'name' => 'scope',
|
||||
'sort' => 'scope',
|
||||
|
@ -76,14 +92,9 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'element' => 'user'
|
||||
],
|
||||
[
|
||||
'name' => 'description',
|
||||
'sort' => 'description',
|
||||
'data_path' => 'description',
|
||||
],
|
||||
[
|
||||
'name' => 'comment',
|
||||
'sort' => 'comment',
|
||||
'data_path' => 'comment',
|
||||
'name' => 'message',
|
||||
'sort' => 'message',
|
||||
'data_path' => 'message',
|
||||
],
|
||||
],
|
||||
'title' => __('Inbox'),
|
||||
|
@ -105,7 +116,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'open_modal' => '/inbox/delete/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'trash',
|
||||
'title' => __('Discard request')
|
||||
'title' => __('Discard message')
|
||||
],
|
||||
]
|
||||
]
|
||||
|
@ -113,7 +124,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
?>
|
||||
|
||||
<script>
|
||||
function discardRequests(idList, selectedData, $table) {
|
||||
function discardMessages(idList, selectedData, $table) {
|
||||
const successCallback = function([data, modalObject]) {
|
||||
UI.reload('/inbox/index', UI.getContainerForTable($table), $table)
|
||||
}
|
||||
|
@ -157,6 +168,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
function handleMessageTable($modal, header, data) {
|
||||
const $modalBody = $modal.find('.modal-body')
|
||||
const $messageTable = $modalBody.find('table.message-table')
|
||||
|
|
|
@ -33,8 +33,8 @@ echo $this->element(
|
|||
'path' => 'user_id',
|
||||
],
|
||||
[
|
||||
'key' => 'description',
|
||||
'path' => 'description',
|
||||
'key' => 'message',
|
||||
'path' => 'message',
|
||||
],
|
||||
[
|
||||
'key' => 'comment',
|
||||
|
|
|
@ -30,6 +30,14 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'data_path' => 'multiple',
|
||||
'field' => 'textarea'
|
||||
],
|
||||
[
|
||||
'name' => __('Sane defaults'),
|
||||
'data_path' => 'sane_default'
|
||||
],
|
||||
[
|
||||
'name' => __('Values List'),
|
||||
'data_path' => 'values_list'
|
||||
],
|
||||
[
|
||||
'name' => __('Validation regex'),
|
||||
'sort' => 'regex',
|
||||
|
|
|
@ -49,6 +49,18 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'data_path' => 'created',
|
||||
'element' => 'datetime'
|
||||
],
|
||||
[
|
||||
'name' => 'severity',
|
||||
'sort' => 'severity',
|
||||
'data_path' => 'severity',
|
||||
'element' => 'function',
|
||||
'function' => function ($entry, $context) {
|
||||
return $context->Bootstrap->badge([
|
||||
'text' => $entry->severity_variant,
|
||||
'variant' => $entry->severity_variant,
|
||||
]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'name' => 'scope',
|
||||
'sort' => 'scope',
|
||||
|
@ -71,14 +83,9 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'element' => 'user'
|
||||
],
|
||||
[
|
||||
'name' => 'description',
|
||||
'sort' => 'description',
|
||||
'data_path' => 'description',
|
||||
],
|
||||
[
|
||||
'name' => 'comment',
|
||||
'sort' => 'comment',
|
||||
'data_path' => 'comment',
|
||||
'name' => 'message',
|
||||
'sort' => 'message',
|
||||
'data_path' => 'message',
|
||||
],
|
||||
],
|
||||
'title' => __('Outbox'),
|
||||
|
@ -100,7 +107,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'open_modal' => '/outbox/delete/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'trash',
|
||||
'title' => __('Discard request')
|
||||
'title' => __('Discard message')
|
||||
],
|
||||
]
|
||||
]
|
||||
|
|
|
@ -29,8 +29,8 @@ echo $this->element(
|
|||
'path' => 'user_id',
|
||||
],
|
||||
[
|
||||
'key' => 'description',
|
||||
'path' => 'description',
|
||||
'key' => 'message',
|
||||
'path' => 'message',
|
||||
],
|
||||
[
|
||||
'key' => 'comment',
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
],
|
||||
[
|
||||
'field' => 'comment',
|
||||
'label' => 'Comment'
|
||||
'label' => 'Comment',
|
||||
'type' => 'textarea',
|
||||
]
|
||||
],
|
||||
'submit' => [
|
||||
|
|
|
@ -18,7 +18,7 @@ echo $this->element(
|
|||
],
|
||||
[
|
||||
'key' => __('Limit'),
|
||||
'path' => 'limit'
|
||||
'path' => 'max_occurrence'
|
||||
],
|
||||
[
|
||||
'key' => __('Comment'),
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Cake\Core\Configure;
|
||||
|
||||
echo $this->element('genericElements/IndexTable/index_table', [
|
||||
'data' => [
|
||||
'data' => $data,
|
||||
|
@ -92,6 +95,12 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'url' => '/user-settings/index?Users.id={{url_data}}',
|
||||
'url_data_path' => 'id'
|
||||
],
|
||||
// [ // We might want to uncomment this at some point
|
||||
// 'name' => __('Keycloak status'),
|
||||
// 'element' => 'keycloak_status',
|
||||
// 'data_path' => 'keycloak_status',
|
||||
// 'requirements' => Configure::read('keycloak.enabled', false),
|
||||
// ],
|
||||
],
|
||||
'title' => __('User index'),
|
||||
'description' => __('The list of enrolled users in this Cerebrate instance. All of the users have or at one point had access to the system.'),
|
||||
|
|
|
@ -51,14 +51,22 @@ $fields = [
|
|||
'scope' => 'individuals'
|
||||
]
|
||||
];
|
||||
if ($keycloakConfig['enabled'] && $loggedUser['id'] == $entity['id']) {
|
||||
if ($keycloakConfig['enabled']) {
|
||||
$fields[] = [
|
||||
'type' => 'generic',
|
||||
'key' => __('Modify keycloak profile'),
|
||||
'path' => 'username',
|
||||
'url' => $kcurl,
|
||||
'requirements' => false
|
||||
'key' => __('Keycloak status'),
|
||||
'type' => 'keycloakStatus',
|
||||
'path' => 'keycloak_status',
|
||||
'requirements' => !empty($keycloakConfig['enabled']),
|
||||
];
|
||||
if ($loggedUser['id'] == $entity['id']) {
|
||||
$fields[] = [
|
||||
'type' => 'generic',
|
||||
'key' => __('Modify keycloak profile'),
|
||||
'path' => 'username',
|
||||
'url' => $kcurl,
|
||||
'requirements' => false
|
||||
];
|
||||
}
|
||||
}
|
||||
echo $this->element(
|
||||
'/genericElements/SingleViews/single_view',
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
if (!empty($element)) {
|
||||
echo $this->Bootstrap->{$element}([
|
||||
'text' => $text,
|
||||
'variant' => $variant,
|
||||
]);
|
||||
}
|
|
@ -1,14 +1,80 @@
|
|||
<?php
|
||||
$controlParams = [
|
||||
'options' => $fieldData['options'],
|
||||
'empty' => $fieldData['empty'] ?? false,
|
||||
'value' => $fieldData['value'] ?? null,
|
||||
'multiple' => $fieldData['multiple'] ?? false,
|
||||
'disabled' => $fieldData['disabled'] ?? false,
|
||||
'class' => ($fieldData['class'] ?? '') . ' formDropdown form-select',
|
||||
'default' => ($fieldData['default'] ?? null)
|
||||
];
|
||||
if (!empty($fieldData['label'])) {
|
||||
$controlParams['label'] = $fieldData['label'];
|
||||
$seed = 's-' . mt_rand();
|
||||
$controlParams = [
|
||||
'type' => 'select',
|
||||
'options' => $fieldData['options'],
|
||||
'empty' => $fieldData['empty'] ?? false,
|
||||
'value' => $fieldData['value'] ?? null,
|
||||
'multiple' => $fieldData['multiple'] ?? false,
|
||||
'disabled' => $fieldData['disabled'] ?? false,
|
||||
'class' => ($fieldData['class'] ?? '') . ' formDropdown form-select',
|
||||
'default' => $fieldData['default'] ?? '',
|
||||
];
|
||||
if (!empty($fieldData['field'])) { // used for multi meta-field form
|
||||
$controlParams['field'] = $fieldData['field'];
|
||||
}
|
||||
if (!empty($fieldData['label'])) {
|
||||
$controlParams['label'] = $fieldData['label'];
|
||||
}
|
||||
if ($controlParams['options'] instanceof \Cake\ORM\Query) {
|
||||
$controlParams['options'] = $controlParams['options']->all()->toList();
|
||||
}
|
||||
if (in_array('_custom', array_keys($controlParams['options']))) {
|
||||
$customInputValue = $this->Form->getSourceValue($fieldData['field']);
|
||||
if (!in_array($customInputValue, $controlParams['options'])) {
|
||||
$controlParams['options'] = array_map(function ($option) {
|
||||
if (is_array($option) && $option['value'] == '_custom') {
|
||||
$option[] = 'selected';
|
||||
}
|
||||
return $option;
|
||||
}, $controlParams['options']);
|
||||
} else {
|
||||
$customInputValue = '';
|
||||
}
|
||||
echo $this->FormFieldMassage->prepareFormElement($this->Form, $controlParams, $fieldData);
|
||||
$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>',
|
||||
sprintf('<input type="text" class="form-control custom-value" field="%s" value="%s">', h($adaptedField), h($customInputValue))
|
||||
);
|
||||
}
|
||||
echo $this->FormFieldMassage->prepareFormElement($this->Form, $controlParams, $fieldData);
|
||||
?>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
$(document).ready(function() {
|
||||
const $select = $('select.dropdown-custom-value-<?= $seed ?>')
|
||||
toggleFreetextSelectField($select[0]);
|
||||
$select.attr('onclick', 'toggleFreetextSelectField(this)')
|
||||
$select.parent().find('input.custom-value').attr('oninput', 'updateAssociatedSelect(this)')
|
||||
updateAssociatedSelect($select.parent().find('input.custom-value')[0])
|
||||
})
|
||||
|
||||
})()
|
||||
|
||||
function toggleFreetextSelectField(selectEl) {
|
||||
const $select = $(selectEl)
|
||||
const show = $select.find('option:selected').hasClass('custom-value')
|
||||
const $container = $(selectEl).parent()
|
||||
let $freetextInput = $container.find('input.custom-value')
|
||||
if (show) {
|
||||
$freetextInput.removeClass('d-none')
|
||||
} else {
|
||||
$freetextInput.addClass('d-none')
|
||||
}
|
||||
}
|
||||
|
||||
function updateAssociatedSelect(input) {
|
||||
const $input = $(input)
|
||||
const $select = $input.parent().find('select')
|
||||
const $customOption = $select.find('option.custom-value')
|
||||
$customOption.val($input.val())
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
form div.form-dropdown-with-freetext input.custom-value {
|
||||
flex-grow: 3;
|
||||
}
|
||||
</style>
|
|
@ -1,11 +1,10 @@
|
|||
<?php
|
||||
if (is_array($fieldData)) {
|
||||
if (empty($fieldData['type'])) {
|
||||
$fieldData['type'] = 'text';
|
||||
}
|
||||
$fieldTemplate = 'genericField';
|
||||
if (file_exists(ROOT . '/templates/element/genericElements/Form/Fields/' . $fieldData['type'] . 'Field.php')) {
|
||||
$fieldTemplate = $fieldData['type'] . 'Field';
|
||||
if (!empty($fieldData['type'])) {
|
||||
if (file_exists(ROOT . '/templates/element/genericElements/Form/Fields/' . $fieldData['type'] . 'Field.php')) {
|
||||
$fieldTemplate = $fieldData['type'] . 'Field';
|
||||
}
|
||||
}
|
||||
if (empty($fieldData['label'])) {
|
||||
if (!isset($fieldData['label']) || $fieldData['label'] !== false) {
|
||||
|
|
|
@ -31,15 +31,16 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
|
|||
$metaField = reset($metaTemplateField->metaFields);
|
||||
$fieldData = [
|
||||
'label' => $metaTemplateField->label,
|
||||
'type' => $metaTemplateField->formType,
|
||||
];
|
||||
if (!empty($metaTemplateField->formOptions)) {
|
||||
$fieldData = array_merge_recursive($fieldData, $metaTemplateField->formOptions);
|
||||
}
|
||||
if (isset($metaField->id)) {
|
||||
$fieldData['field'] = sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.%s.value', $metaField->meta_template_id, $metaField->meta_template_field_id, $metaField->id);
|
||||
} else {
|
||||
$fieldData['field'] = sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.%s.value', $metaField->meta_template_id, $metaField->meta_template_field_id, array_key_first($metaTemplateField->metaFields));
|
||||
}
|
||||
if ($metaTemplateField->type === 'boolean') {
|
||||
$fieldData['type'] = 'checkbox';
|
||||
}
|
||||
$this->Form->setTemplates($backupTemplates);
|
||||
$fieldsHtml .= $this->element(
|
||||
'genericElements/Form/fieldScaffold',
|
||||
|
@ -66,9 +67,10 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
|
|||
$fieldData = [
|
||||
'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.new.0', $metaTemplateField->meta_template_id, $metaTemplateField->id),
|
||||
'label' => $metaTemplateField->label,
|
||||
'type' => $metaTemplateField->formType,
|
||||
];
|
||||
if ($metaTemplateField->type === 'boolean') {
|
||||
$fieldData['type'] = 'checkbox';
|
||||
if (!empty($metaTemplateField->formOptions)) {
|
||||
$fieldData = array_merge_recursive($fieldData, $metaTemplateField->formOptions);
|
||||
}
|
||||
$fieldsHtml .= $this->element(
|
||||
'genericElements/Form/fieldScaffold',
|
||||
|
@ -80,4 +82,7 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
|
|||
}
|
||||
}
|
||||
}
|
||||
echo $fieldsHtml;
|
||||
$fieldContainer = $this->Bootstrap->genNode('div', [
|
||||
'class' => [],
|
||||
], $fieldsHtml);
|
||||
echo $fieldContainer;
|
|
@ -45,6 +45,8 @@ $seed = 'mfb-' . mt_rand();
|
|||
$clonedContainer
|
||||
.removeClass('has-error')
|
||||
.find('.error-message ').remove()
|
||||
$clonedContainer
|
||||
.find('label.form-label').text('')
|
||||
const $clonedInput = $clonedContainer.find('input, select')
|
||||
if ($clonedInput.length > 0) {
|
||||
const injectedTemplateId = $clicked.closest('.multi-metafields-container').find('.new-metafield').length
|
||||
|
@ -54,18 +56,25 @@ $seed = 'mfb-' . mt_rand();
|
|||
}
|
||||
}
|
||||
|
||||
function adjustClonedInputAttr($input, injectedTemplateId) {
|
||||
let explodedPath = $input.attr('field').split('.').splice(0, 5)
|
||||
explodedPath.push('new', injectedTemplateId)
|
||||
dottedPathStr = explodedPath.join('.')
|
||||
brackettedPathStr = explodedPath.map((elem, i) => {
|
||||
return i == 0 ? elem : `[${elem}]`
|
||||
}).join('')
|
||||
$input.attr('id', dottedPathStr)
|
||||
.attr('field', dottedPathStr)
|
||||
.attr('name', brackettedPathStr)
|
||||
.val('')
|
||||
.removeClass('is-invalid')
|
||||
function adjustClonedInputAttr($inputs, injectedTemplateId) {
|
||||
$inputs.each(function() {
|
||||
const $input = $(this)
|
||||
let explodedPath = $input.attr('field').split('.').splice(0, 5)
|
||||
explodedPath.push('new', injectedTemplateId)
|
||||
dottedPathStr = explodedPath.join('.')
|
||||
brackettedPathStr = explodedPath.map((elem, i) => {
|
||||
return i == 0 ? elem : `[${elem}]`
|
||||
}).join('')
|
||||
const attrs = ['id', 'field', 'name']
|
||||
attrs.forEach((attr) => {
|
||||
if ($input.attr(attr) !== undefined) {
|
||||
$input.attr(attr, attr === 'name' ? brackettedPathStr : dottedPathStr)
|
||||
}
|
||||
})
|
||||
$input
|
||||
.val('')
|
||||
.removeClass('is-invalid')
|
||||
})
|
||||
}
|
||||
})()
|
||||
</script>
|
|
@ -22,6 +22,7 @@ if (!empty($metaFieldsEntities)) {
|
|||
$metaFieldsEntity->meta_template_field_id,
|
||||
$metaFieldsEntity->id
|
||||
),
|
||||
'type' => $metaTemplateField->formType,
|
||||
];
|
||||
if($metaFieldsEntity->isNew()) {
|
||||
$fieldData['field'] = sprintf(
|
||||
|
@ -33,7 +34,10 @@ if (!empty($metaFieldsEntities)) {
|
|||
$fieldData['class'] = 'new-metafield';
|
||||
}
|
||||
if ($labelPrintedOnce) { // Only the first input can have a label
|
||||
$fieldData['label'] = false;
|
||||
$fieldData['label'] = ['text' => ''];
|
||||
}
|
||||
if ($metaTemplateField->formType === 'dropdown') {
|
||||
$fieldData = array_merge_recursive($fieldData, $metaTemplateField->formOptions);
|
||||
}
|
||||
$labelPrintedOnce = true;
|
||||
$fieldsHtml .= $this->element(
|
||||
|
@ -49,14 +53,19 @@ if (!empty($metaTemplateField) && !empty($multiple)) { // Add multiple field but
|
|||
$metaTemplateField->label = Inflector::humanize($metaTemplateField->field);
|
||||
$emptyMetaFieldInput = '';
|
||||
if (empty($metaFieldsEntities)) { // Include editable field for meta-template not containing a meta-field
|
||||
$fieldData = [
|
||||
'label' => $metaTemplateField->label,
|
||||
'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.new.0', $metaTemplateField->meta_template_id, $metaTemplateField->id),
|
||||
'class' => 'new-metafield',
|
||||
'type' => $metaTemplateField->formType,
|
||||
];
|
||||
if ($metaTemplateField->formType === 'dropdown') {
|
||||
$fieldData = array_merge_recursive($fieldData, $metaTemplateField->formOptions);
|
||||
}
|
||||
$emptyMetaFieldInput = $this->element(
|
||||
'genericElements/Form/fieldScaffold',
|
||||
[
|
||||
'fieldData' => [
|
||||
'label' => $metaTemplateField->label,
|
||||
'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.new.0', $metaTemplateField->meta_template_id, $metaTemplateField->id),
|
||||
'class' => 'new-metafield',
|
||||
],
|
||||
'fieldData' => $fieldData,
|
||||
'form' => $form,
|
||||
]
|
||||
);
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
$data = $this->Hash->get($row, $field['data_path']);
|
||||
if (is_null($data)) {
|
||||
echo '';
|
||||
} else if (!empty($data['require_update'])) {
|
||||
echo sprintf(
|
||||
'<span data-bs-toggle="tooltip" data-bs-title="%s">%s</span>',
|
||||
sprintf('Fields having differences: %s', (implode(', ', array_keys($data['differences'])))),
|
||||
$this->Bootstrap->icon('times', ['class' => 'text-danger', ])
|
||||
);
|
||||
} else {
|
||||
echo $this->Bootstrap->icon('check', ['class' => 'text-success',]);
|
||||
}
|
||||
?>
|
|
@ -25,10 +25,12 @@ if (!empty($requestedMetaFields)) { // Create mapping for new index table fields
|
|||
foreach ($requestedMetaFields as $requestedMetaField) {
|
||||
$template_id = $requestedMetaField['template_id'];
|
||||
$meta_template_field_id = $requestedMetaField['meta_template_field_id'];
|
||||
$viewElementCandidate = $meta_templates[$template_id]['meta_template_fields'][$meta_template_field_id]['index_type'];
|
||||
$viewElementCandidatePath = '/genericElements/IndexTable/Fields/' . $viewElementCandidate;
|
||||
$newMetaFields[] = [
|
||||
'name' => $meta_templates[$template_id]['meta_template_fields'][$meta_template_field_id]['field'],
|
||||
'data_path' => "MetaTemplates.{$template_id}.meta_template_fields.{$meta_template_field_id}.metaFields.{n}.value",
|
||||
'element' => 'generic_field',
|
||||
'element' => $this->elementExists($viewElementCandidatePath) ? $viewElementCandidate : 'generic_field',
|
||||
'_metafield' => true,
|
||||
'_automatic_field' => true,
|
||||
];
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
} else {
|
||||
$currentFilteringContext = $filteringContext['filterCondition'];
|
||||
}
|
||||
$contextArray[] = [
|
||||
$contextItem = [
|
||||
'active' => (
|
||||
(
|
||||
$currentQuery == $currentFilteringContext && // query conditions match
|
||||
|
@ -42,9 +42,17 @@
|
|||
"#table-container-${tableRandomValue}",
|
||||
"#table-container-${tableRandomValue} table.table",
|
||||
],
|
||||
'text' => $filteringContext['label'],
|
||||
'class' => 'btn-sm'
|
||||
];
|
||||
if (!empty($filteringContext['viewElement'])) {
|
||||
$contextItem['html'] = $this->element(
|
||||
$filteringContext['viewElement'],
|
||||
$filteringContext['viewElementParams'] ?? []
|
||||
);
|
||||
} else {
|
||||
$contextItem['text'] = $filteringContext['label'];
|
||||
}
|
||||
$contextArray[] = $contextItem;
|
||||
}
|
||||
|
||||
$dataGroup = [
|
||||
|
|
|
@ -8,6 +8,7 @@ if (empty($data['table_setting_id']) && empty($model)) {
|
|||
$data['table_setting_id'] = !empty($data['table_setting_id']) ? $data['table_setting_id'] : IndexSetting::getIDFromTable($model);
|
||||
$tableSettings = IndexSetting::getTableSetting($loggedUser, $data['table_setting_id']);
|
||||
$compactDisplay = !empty($tableSettings['compact_display']);
|
||||
$numberOfElement = $tableSettings['number_of_element'] ?? 20;
|
||||
|
||||
$availableColumnsHtml = $this->element('/genericElements/ListTopBar/group_table_action/hiddenColumns', [
|
||||
'table_data' => $table_data,
|
||||
|
@ -51,7 +52,13 @@ $compactDisplayHtml = $this->element('/genericElements/ListTopBar/group_table_ac
|
|||
'table_data' => $table_data,
|
||||
'tableSettings' => $tableSettings,
|
||||
'table_setting_id' => $data['table_setting_id'],
|
||||
'compactDisplay' => $compactDisplay
|
||||
'compactDisplay' => $compactDisplay,
|
||||
]);
|
||||
$numberOfElementHtml = $this->element('/genericElements/ListTopBar/group_table_action/numberOfElement', [
|
||||
'table_data' => $table_data,
|
||||
'tableSettings' => $tableSettings,
|
||||
'table_setting_id' => $data['table_setting_id'],
|
||||
'numberOfElement' => $numberOfElement,
|
||||
]);
|
||||
?>
|
||||
<?php if (!isset($data['requirement']) || $data['requirement']) : ?>
|
||||
|
@ -63,6 +70,7 @@ $compactDisplayHtml = $this->element('/genericElements/ListTopBar/group_table_ac
|
|||
'toggle-button' => [
|
||||
'icon' => 'sliders-h',
|
||||
'variant' => 'primary',
|
||||
'class' => ['table_setting_dropdown_button'],
|
||||
],
|
||||
'submenu_alignment' => 'end',
|
||||
'submenu_direction' => 'start',
|
||||
|
@ -79,8 +87,22 @@ $compactDisplayHtml = $this->element('/genericElements/ListTopBar/group_table_ac
|
|||
],
|
||||
[
|
||||
'html' => $compactDisplayHtml,
|
||||
],
|
||||
[
|
||||
'html' => $numberOfElementHtml,
|
||||
]
|
||||
]
|
||||
]);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
const dropdownBtn = document.querySelector('button.table_setting_dropdown_button')
|
||||
dropdownBtn.addEventListener('hidden.bs.dropdown', function() {
|
||||
const $dropdownBtn = $(this)
|
||||
const debouncedFunctions = $dropdownBtn.data('debouncedFunctions')
|
||||
firePendingDebouncedFunctions(dropdownBtn)
|
||||
})
|
||||
})
|
||||
</script>
|
|
@ -41,6 +41,7 @@ $compactDisplayInputSeed = 'seed-' . mt_rand();
|
|||
const $container = $dropdownMenu.closest('div[id^="table-container-"]')
|
||||
const $table = $container.find(`table[data-table-random-value="${tableRandomValue}"]`)
|
||||
toggleCompactState($checkbox[0].checked, $table)
|
||||
registerDebouncedFunction($container, debouncedCompactSaver)
|
||||
})
|
||||
})()
|
||||
</script>
|
|
@ -40,7 +40,8 @@ echo $availableColumnsHtml;
|
|||
const debouncedHiddenColumnSaverWithReload = debounce(mergeAndSaveSettingsWithReload, 2000)
|
||||
|
||||
function attachListeners() {
|
||||
let debouncedFunctionWithReload = false, debouncedFunction = false // used to flush debounce function if dropdown menu gets closed
|
||||
let debouncedFunctionWithReload = false,
|
||||
debouncedFunction = false // used to flush debounce function if dropdown menu gets closed
|
||||
$('form.visible-column-form, form.visible-meta-column-form').find('input').change(function() {
|
||||
const $dropdownMenu = $(this).closest(`[data-table-random-value]`)
|
||||
const tableRandomValue = $dropdownMenu.attr('data-table-random-value')
|
||||
|
@ -108,24 +109,6 @@ echo $availableColumnsHtml;
|
|||
return tableSetting
|
||||
}
|
||||
|
||||
function mergeAndSaveSettingsWithReload(table_setting_id, tableSettings, $table) {
|
||||
mergeAndSaveSettings(table_setting_id, tableSettings, false).then((apiResult) => {
|
||||
const theToast = UI.toast({
|
||||
variant: 'success',
|
||||
title: apiResult.message,
|
||||
bodyHtml: $('<div/>').append(
|
||||
$('<span/>').text('<?= __('The table needs to be reloaded for the new fields to be included.') ?>'),
|
||||
$('<button/>').addClass(['btn', 'btn-primary', 'btn-sm', 'ms-3']).text('<?= __('Reload table') ?>').click(function() {
|
||||
const reloadUrl = $table.data('reload-url');
|
||||
UI.reload(reloadUrl, $table.closest('div[id^="table-container-"]'), $(this)).then(() => {
|
||||
theToast.removeToast()
|
||||
})
|
||||
}),
|
||||
),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
addSupportOfNestedDropdown();
|
||||
const $form = $('form.visible-column-form, form.visible-meta-column-form')
|
||||
|
@ -138,6 +121,8 @@ echo $availableColumnsHtml;
|
|||
toggleColumn(this.getAttribute('data-columnname'), this.checked, $table)
|
||||
})
|
||||
attachListeners()
|
||||
registerDebouncedFunction($container, debouncedHiddenColumnSaver)
|
||||
registerDebouncedFunction($container, debouncedHiddenColumnSaverWithReload)
|
||||
})
|
||||
})()
|
||||
</script>
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
$numberOfElementSelectSeed = 'seed-' . mt_rand();
|
||||
?>
|
||||
|
||||
<label class="dropdown-item d-flex align-items-center cursor-pointer" href="#" for="<?= $numberOfElementSelectSeed ?>">
|
||||
<span class="fw-bold">#</span>
|
||||
<span class="ms-2"><?= __('Show') ?></span>
|
||||
<select id="<?= $numberOfElementSelectSeed ?>" class="select-number-of-element form-select ms-auto" style="width: 5em;">
|
||||
<option value="20" <?= $numberOfElement == 20 ? 'selected' : '' ?>><?= __('20') ?></option>
|
||||
<option value="50" <?= $numberOfElement == 50 ? 'selected' : '' ?>><?= __('50') ?></option>
|
||||
<option value="100" <?= $numberOfElement == 100 ? 'selected' : '' ?>><?= __('100') ?></option>
|
||||
<option value="200" <?= $numberOfElement == 200 ? 'selected' : '' ?>><?= __('200') ?></option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const debouncedNumberElementSaver = debounce(mergeAndSaveSettingsWithReload, 2000)
|
||||
|
||||
$('#<?= $numberOfElementSelectSeed ?>').change(function() {
|
||||
const $dropdownMenu = $(this).closest('.dropdown')
|
||||
const tableRandomValue = $dropdownMenu.attr('data-table-random-value')
|
||||
const $container = $dropdownMenu.closest('div[id^="table-container-"]')
|
||||
const $table = $container.find(`table[data-table-random-value="${tableRandomValue}"]`)
|
||||
const table_setting_id = $dropdownMenu.data('table_setting_id');
|
||||
let newTableSettings = {}
|
||||
newTableSettings[table_setting_id] = {
|
||||
'number_of_element': $(this).val()
|
||||
}
|
||||
debouncedNumberElementSaver(table_setting_id, newTableSettings, $table)
|
||||
})
|
||||
|
||||
$(document).ready(function() {
|
||||
const $select = $('#<?= $numberOfElementSelectSeed ?>')
|
||||
const $dropdownMenu = $select.closest('.dropdown')
|
||||
const tableRandomValue = $dropdownMenu.attr('data-table-random-value')
|
||||
const $container = $dropdownMenu.closest('div[id^="table-container-"]')
|
||||
const $table = $container.find(`table[data-table-random-value="${tableRandomValue}"]`)
|
||||
registerDebouncedFunction($container, debouncedNumberElementSaver)
|
||||
})
|
||||
})()
|
||||
</script>
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
use Cake\Utility\Hash;
|
||||
|
||||
$value = Hash::get($data, $field['path']);
|
||||
$differencesRearranged = array_map(function($difference) {
|
||||
return [
|
||||
__('Local: {0}', h($difference['cerebrate'])),
|
||||
__('Keycloak: {0}', h($difference['kc'])),
|
||||
];
|
||||
}, $value['differences']);
|
||||
if (!empty($value['require_update'])) {
|
||||
echo sprintf(
|
||||
'<div class="alert alert-warning"><div>%s</div>%s</div>',
|
||||
$this->Bootstrap->icon('exclamation-triangle') . __(' This user is not synchronise with Keycloak. Differences:'),
|
||||
$this->Html->nestedList($differencesRearranged, ['class' => ''])
|
||||
);
|
||||
} else {
|
||||
echo $this->Bootstrap->icon('check', ['class' => 'text-success',]);
|
||||
}
|
||||
?>
|
|
@ -21,7 +21,7 @@ $variant = empty($notification['variant']) ? 'primary' : $notification['variant'
|
|||
</span>
|
||||
<?php endif; ?>
|
||||
<span class="notification-text-container">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex justify-content-between align-items-center gap-2">
|
||||
<span class="notification-title text-truncate"><?= $this->ValueGetter->get($notification['text']) ?></span>
|
||||
<?php if (!empty($notification['datetime'])) : ?>
|
||||
<small id="<?= $seed ?>" class="notification-date text-muted fw-light"><?= h($notification['datetime']->format('Y-m-d\TH:i:s')) ?></small>
|
||||
|
|
|
@ -7,6 +7,7 @@ $tableItems = array_map(function ($fieldName) {
|
|||
'fieldname' => $fieldName,
|
||||
];
|
||||
}, $filters);
|
||||
$formTypeMap = $this->Form->getConfig('typeMap');
|
||||
|
||||
$filteringForm = $this->Bootstrap->table(
|
||||
[
|
||||
|
@ -23,11 +24,19 @@ $filteringForm = $this->Bootstrap->table(
|
|||
}
|
||||
],
|
||||
[
|
||||
'key' => 'operator', 'label' => __('Operator'), 'formatter' => function ($field, $row) {
|
||||
'key' => 'operator', 'label' => __('Operator'), 'formatter' => function ($field, $row) use ($typeMap) {
|
||||
$fieldName = $row['fieldname'];
|
||||
$type = $typeMap[$fieldName] ?? 'text';
|
||||
$options = [
|
||||
sprintf('<option value="%s">%s</option>', '=', '='),
|
||||
sprintf('<option value="%s">%s</option>', '!=', '!='),
|
||||
];
|
||||
if ($type === 'datetime') {
|
||||
$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));
|
||||
}
|
||||
],
|
||||
|
@ -38,8 +47,21 @@ $filteringForm = $this->Bootstrap->table(
|
|||
__('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">');
|
||||
'formatter' => function ($field, $row) use ($typeMap, $formTypeMap) {
|
||||
$fieldName = $row['fieldname'];
|
||||
$formType = $formTypeMap[$typeMap[$fieldName]] ?? 'text';
|
||||
$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'
|
||||
],
|
||||
'params' => []
|
||||
]);
|
||||
}
|
||||
],
|
||||
],
|
||||
|
@ -102,8 +124,8 @@ echo $this->Bootstrap->modal([
|
|||
$rows.each(function() {
|
||||
const rowData = getDataFromRow($(this))
|
||||
let fullFilter = rowData['name']
|
||||
if (rowData['operator'] == '!=') {
|
||||
fullFilter += ' !='
|
||||
if (rowData['operator'] != '=') {
|
||||
fullFilter += ` ${rowData['operator']}`
|
||||
}
|
||||
if (rowData['value'].length > 0) {
|
||||
activeFilters[fullFilter] = rowData['value']
|
||||
|
@ -111,14 +133,13 @@ echo $this->Bootstrap->modal([
|
|||
})
|
||||
if (modalObject.$modal.find('table.indexMetaFieldsFilteringTable').length > 0) {
|
||||
let metaFieldFilters = modalObject.$modal.find('table.indexMetaFieldsFilteringTable')[0].getFiltersFunction()
|
||||
// activeFilters['filteringMetaFields'] = metaFieldFilters !== undefined ? metaFieldFilters : [];
|
||||
metaFieldFilters = metaFieldFilters !== undefined ? metaFieldFilters : []
|
||||
for (let [metaFieldPath, metaFieldValue] of Object.entries(metaFieldFilters)) {
|
||||
activeFilters[metaFieldPath] = metaFieldValue
|
||||
}
|
||||
}
|
||||
$selectTag = modalObject.$modal.find('.tag-container select.select2-input')
|
||||
activeFilters['filteringTags'] = $selectTag.select2('data').map(tag => tag.text)
|
||||
activeFilters['filteringTags'] = $selectTag.length > 0 ? $selectTag.select2('data').map(tag => tag.text) : []
|
||||
const searchParam = jQuery.param(activeFilters);
|
||||
const url = `/${controller}/${action}?${searchParam}`
|
||||
|
||||
|
@ -138,8 +159,8 @@ echo $this->Bootstrap->modal([
|
|||
for (let [field, value] of Object.entries(activeFilters)) {
|
||||
const fieldParts = field.split(' ')
|
||||
let operator = '='
|
||||
if (fieldParts.length == 2 && fieldParts[1] == '!=') {
|
||||
operator = '!='
|
||||
if (fieldParts.length == 2) {
|
||||
operator = fieldParts[1]
|
||||
field = fieldParts[0]
|
||||
} else if (fieldParts.length > 2) {
|
||||
console.error('Field contains multiple spaces. ' + field)
|
||||
|
@ -167,14 +188,24 @@ echo $this->Bootstrap->modal([
|
|||
return $(this).data('fieldname') == field
|
||||
}).closest('tr')
|
||||
$row.find('.fieldOperator').val(operator)
|
||||
$row.find('.fieldValue').val(value)
|
||||
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')
|
||||
rowData['operator'] = $row.find('select.fieldOperator').val()
|
||||
rowData['value'] = $row.find('input.fieldValue').val()
|
||||
const $formElement = $row.find('.fieldValue');
|
||||
if ($formElement.attr('type') === 'datetime-local') {
|
||||
rowData['value'] = $formElement.val().length > 0 ? moment($formElement.val()).toISOString() : $formElement.val()
|
||||
} else {
|
||||
rowData['value'] = $formElement.val()
|
||||
}
|
||||
return rowData
|
||||
}
|
||||
|
||||
|
|
|
@ -88,7 +88,7 @@ $sidebarOpen = $loggedUser->user_settings_by_name_with_fallback['ui.sidebar.expa
|
|||
</main>
|
||||
</div>
|
||||
<div id="mainModal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mediumModalLabel" aria-hidden="true"></div>
|
||||
<div id="mainToastContainer" style="position: absolute; top: 15px; right: 15px; z-index: 1080"></div>
|
||||
<div id="mainToastContainer" class="main-toast-container"></div>
|
||||
<div id="mainModalContainer"></div>
|
||||
</body>
|
||||
|
||||
|
|
|
@ -207,6 +207,15 @@ main.content {
|
|||
min-width: 25px;
|
||||
}
|
||||
|
||||
.main-toast-container {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
z-index: 1080;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.5em;
|
||||
}
|
||||
|
||||
/* sidebar */
|
||||
.sidebar-wrapper {
|
||||
|
@ -537,9 +546,10 @@ ul.sidebar-elements > li.category > span.category-divider > hr {
|
|||
}
|
||||
.navbar .right-navbar .notification-menu a.notification-item .notification-text-container {
|
||||
max-width: calc(100% - 40px - 0.25rem);
|
||||
flex-grow: 1;
|
||||
}
|
||||
.navbar .right-navbar .notification-menu a.notification-item .notification-title {
|
||||
max-width: calc(100% - 90px);
|
||||
max-width: 300px;
|
||||
}
|
||||
.navbar .right-navbar .notification-menu a.notification-item .notification-date {
|
||||
max-width: 90px;
|
||||
|
|
|
@ -381,7 +381,7 @@ class AJAXApi {
|
|||
this.provideSuccessFeedback(data, {}, skipFeedback)
|
||||
toReturn = data;
|
||||
} else {
|
||||
this.provideFailureFeedback(data, {}, skipFeedback)
|
||||
this.provideFailureFeedback(data, {}, false)
|
||||
feedbackShown = true
|
||||
this.injectFormValidationFeedback(form, data.errors)
|
||||
toReturn = Promise.reject(data.errors);
|
||||
|
|
|
@ -350,18 +350,16 @@ class Toaster {
|
|||
$toast.attr('id', options.id)
|
||||
}
|
||||
$toast.addClass('toast-' + options.variant)
|
||||
if (options.title !== false || options.titleHtml !== false || options.muted !== false || options.mutedHtml !== false) {
|
||||
if (options.title !== false || options.titleHtml !== false || options.muted !== false || options.mutedHtml !== false || options.closeButton) {
|
||||
var $toastHeader = $('<div class="toast-header"/>')
|
||||
$toastHeader.addClass('toast-' + options.variant)
|
||||
if (options.title !== false || options.titleHtml !== false) {
|
||||
var $toastHeaderText
|
||||
if (options.titleHtml !== false) {
|
||||
$toastHeaderText = $('<div class="me-auto"/>').html(options.titleHtml);
|
||||
} else {
|
||||
$toastHeaderText = $('<strong class="me-auto"/>').text(options.title)
|
||||
}
|
||||
$toastHeader.append($toastHeaderText)
|
||||
let $toastHeaderText = $('<span class="me-auto"/>')
|
||||
if (options.titleHtml !== false) {
|
||||
$toastHeaderText = $('<div class="me-auto"/>').html(options.titleHtml);
|
||||
} else if (options.title !== false) {
|
||||
$toastHeaderText = $('<strong class="me-auto"/>').text(options.title)
|
||||
}
|
||||
$toastHeader.append($toastHeaderText)
|
||||
if (options.muted !== false || options.mutedHtml !== false) {
|
||||
var $toastHeaderMuted
|
||||
if (options.mutedHtml !== false) {
|
||||
|
|
|
@ -67,14 +67,21 @@ function saveAndUpdateSetting(statusNode, $input, settingName, settingValue) {
|
|||
settingValue = JSON.stringify(settingValue)
|
||||
}
|
||||
saveSetting(statusNode, settingName, settingValue).then((result) => {
|
||||
window.settingsFlattened[settingName] = result.data
|
||||
if ($input.attr('type') == 'checkbox') {
|
||||
$input.prop('checked', result.data.value == true)
|
||||
} else {
|
||||
$input.val(result.data.value)
|
||||
}
|
||||
updateSettingValue($input, settingName, result.data)
|
||||
}).catch((e) => {
|
||||
updateSettingValue($input, settingName, window.settingsFlattened[settingName])
|
||||
}).finally(() => {
|
||||
handleSettingValueChange($input)
|
||||
}).catch((e) => { })
|
||||
})
|
||||
}
|
||||
|
||||
function updateSettingValue($input, settingName, settingValue) {
|
||||
window.settingsFlattened[settingName] = settingValue
|
||||
if ($input.attr('type') == 'checkbox') {
|
||||
$input.prop('checked', settingValue.value == true)
|
||||
} else {
|
||||
$input.val(settingValue.value)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSettingValueChange($input) {
|
||||
|
|
|
@ -10,6 +10,41 @@ function mergeAndSaveSettings(table_setting_id, newTableSettings, automaticFeedb
|
|||
})
|
||||
}
|
||||
|
||||
function mergeAndSaveSettingsWithReload(table_setting_id, tableSettings, $table) {
|
||||
mergeAndSaveSettings(table_setting_id, tableSettings, false).then((apiResult) => {
|
||||
const theToast = UI.toast({
|
||||
variant: 'success',
|
||||
title: apiResult.message,
|
||||
bodyHtml: $('<div/>').append(
|
||||
$('<span/>').text('The table needs to be reloaded for the new fields to be included.'),
|
||||
$('<button/>').addClass(['btn', 'btn-primary', 'btn-sm', 'ms-3']).text('Reload table').click(function () {
|
||||
const reloadUrl = $table.data('reload-url');
|
||||
UI.reload(reloadUrl, $table.closest('div[id^="table-container-"]'), $(this)).then(() => {
|
||||
theToast.removeToast()
|
||||
})
|
||||
}),
|
||||
),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function registerDebouncedFunction($container, fn) {
|
||||
$dropdownButton = $container.find('button.table_setting_dropdown_button')
|
||||
if ($dropdownButton.data('debouncedFunctions') === undefined) {
|
||||
$dropdownButton.data('debouncedFunctions', [])
|
||||
}
|
||||
$dropdownButton.data('debouncedFunctions').push(fn)
|
||||
}
|
||||
|
||||
function firePendingDebouncedFunctions(dropdownBtn) {
|
||||
$dropdownButton = $(dropdownBtn)
|
||||
if ($dropdownButton.data('debouncedFunctions') !== undefined) {
|
||||
$dropdownButton.data('debouncedFunctions').forEach(function (fn) {
|
||||
fn.flush()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function mergeNewTableSettingsIntoOld(table_setting_id, oldTableSettings, newTableSettings) {
|
||||
// Merge recursively
|
||||
tableSettings = Object.assign({}, oldTableSettings, newTableSettings)
|
||||
|
|
Loading…
Reference in New Issue