new: [inbox] First version of Inbox system and requestProcessors - WiP

pull/41/head
mokaddem 2021-03-15 22:47:13 +01:00
parent 010d0896a9
commit 77fe4e6505
31 changed files with 881 additions and 32 deletions

View File

@ -0,0 +1,113 @@
<?php
use Cake\ORM\TableRegistry;
use Cake\Filesystem\File;
use Cake\Utility\Inflector;
use Cake\Validation\Validator;
interface GenericProcessorActionI
{
public function create($requestData);
public function process($requestID, $serverRequest);
public function discard($requestID);
public function setViewVariables($controller, $request);
}
class GenericRequestProcessor
{
public $Inbox;
protected $registeredActions = [];
protected $validator;
private $processingTemplate = '/genericTemplates/confirm';
private $processingTemplatesDirectory = ROOT . '/templates/RequestProcessors';
public function __construct($registerActions=false) {
$this->Inbox = TableRegistry::getTableLocator()->get('Inbox');
if ($registerActions) {
$this->registerActionInProcessor();
}
$processingTemplatePath = $this->getProcessingTemplatePath();
$file = new File($this->processingTemplatesDirectory . DS . $processingTemplatePath);
if ($file->exists()) {
$this->processingTemplate = $processingTemplatePath;
}
$file->close();
}
private function getProcessingTemplatePath()
{
$class = str_replace('RequestProcessor', '', get_parent_class($this));
$action = strtolower(str_replace('Processor', '', get_class($this)));
return sprintf('%s/%s.php',
$class,
$action
);
}
public function getProcessingTemplate()
{
if ($this->processingTemplate == '/genericTemplates/confirm') {
return '/genericTemplates/confirm';
}
return DS . 'RequestProcessors' . DS . str_replace('.php', '', $this->processingTemplate);
}
protected function generateRequest($requestData)
{
$request = $this->Inbox->newEmptyEntity();
$request = $this->Inbox->patchEntity($request, $requestData);
if ($request->getErrors()) {
throw new MethodNotAllowed(__('Could not create request.{0}Reason: {1}', PHP_EOL, json_encode($request->getErrors())), 1);
}
return $request;
}
protected function validateRequestData($requestData)
{
$errors = [];
if (!isset($requestData['data'])) {
$errors[] = __('No request data provided');
}
$validator = new Validator();
if (method_exists($this, 'addValidatorRules')) {
$validator = $this->addValidatorRules($validator);
$errors = $validator->validate($requestData['data']);
}
if (!empty($errors)) {
throw new Exception('Error while validating request data. ' . json_encode($errors), 1);
}
}
protected function registerActionInProcessor()
{
foreach ($this->registeredActions as $i => $action) {
$className = "{$action}Processor";
$reflection = new ReflectionClass($className);
if ($reflection->isAbstract() || $reflection->isInterface()) {
throw new Exception(__('Cannot create instance of %s, as it is abstract or is an interface'));
}
$this->{$action} = $reflection->newInstance();
}
}
public function checkLoading()
{
return 'Assimilation successful!';
}
protected function setViewVariablesConfirmModal($controller, $id, $title='', $question='', $actionName='')
{
$controller->set('title', !empty($title) ? $title : __('Process request {0}', $id));
$controller->set('question', !empty($question) ? $question : __('Confirm request {0}', $id));
$controller->set('actionName', !empty($actionName) ? $actionName : __('Confirm'));
$controller->set('path', ['controller' => 'inbox', 'action' => 'process', $id]);
}
public function create($requestData)
{
$request = $this->generateRequest($requestData);
$savedRequest = $this->Inbox->save($request);
if ($savedRequest !== false) {
// log here
}
}
}

View File

@ -0,0 +1,122 @@
<?php
use Cake\ORM\TableRegistry;
require_once(ROOT . DS . 'libraries' . DS . 'RequestProcessors' . DS . 'GenericRequestProcessor.php');
class UserRequestProcessor extends GenericRequestProcessor
{
protected $scope = 'User';
protected $action = 'overridden-in-processor-action';
protected $description = 'overridden-in-processor-action';
protected $registeredActions = [
'Registration'
];
public function __construct($loadFromAction=false) {
parent::__construct($loadFromAction);
}
public function create($requestData)
{
$requestData['scope'] = $this->scope;
$requestData['action'] = $this->action;
$requestData['description'] = $this->description;
parent::create($requestData);
}
}
class RegistrationProcessor extends UserRequestProcessor implements GenericProcessorActionI {
protected $action = 'Registration';
protected $description;
public function __construct() {
parent::__construct();
$this->description = __('Handle user account for this cerebrate instance');
$this->Users = TableRegistry::getTableLocator()->get('Users');
}
protected function addValidatorRules($validator)
{
return $validator
->requirePresence('username')
->notEmpty('name', 'A username must be provided.')
->requirePresence('email')
->add('email', 'validFormat', [
'rule' => 'email',
'message' => 'E-mail must be valid'
])
->requirePresence('first_name')
->notEmpty('name', 'A first name must be provided.')
->requirePresence('last_name')
->notEmpty('name', 'A last name must be provided.');
}
public function create($requestData) {
$this->validateRequestData($requestData);
$requestData['title'] = __('User account creation requested for {0}', $requestData['data']['email']);
parent::create($requestData);
}
public function setViewVariables($controller, $request)
{
$dropdownData = [
'role' => $this->Users->Roles->find('list', [
'sort' => ['name' => 'asc']
]),
'individual' => [-1 => __('-- New individual --')] + $this->Users->Individuals->find('list', [
'sort' => ['email' => 'asc']
])->toArray()
];
$individualEntity = $this->Users->Individuals->newEntity([
'email' => !empty($request['data']['email']) ? $request['data']['email'] : '',
'first_name' => !empty($request['data']['first_name']) ? $request['data']['first_name'] : '',
'last_name' => !empty($request['data']['last_name']) ? $request['data']['last_name'] : '',
'position' => !empty($request['data']['position']) ? $request['data']['position'] : '',
]);
$userEntity = $this->Users->newEntity([
'individual_id' => -1,
'username' => !empty($request['data']['username']) ? $request['data']['username'] : '',
'role_id' => !empty($request['data']['role_id']) ? $request['data']['role_id'] : '',
'disabled' => !empty($request['data']['disabled']) ? $request['data']['disabled'] : '',
]);
$controller->set('individualEntity', $individualEntity);
$controller->set('userEntity', $userEntity);
$controller->set(compact('dropdownData'));
}
public function process($id, $serverRequest)
{
$data = $serverRequest->getData();
if ($data['individual_id'] == -1) {
$individual = $this->Users->Individuals->newEntity([
'uuid' => $data['uuid'],
'email' => $data['email'],
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'position' => $data['position'],
]);
$individual = $this->Users->Individuals->save($individual);
} else {
$individual = $this->Users->Individuals->get($data['individual_id']);
}
$user = $this->Users->newEntity([
'individual_id' => $individual->id,
'username' => $data['username'],
'password' => '~PASSWORD_TO_BE_REPLACED~',
'role_id' => $data['role_id'],
'disabled' => $data['disabled'],
]);
$user = $this->Users->save($user);
return [
'data' => $user,
'success' => $user !== false,
'message' => $user !== false ? __('User `{0}` created', $user->username) : __('Could not create user `{0}`.', $user->username),
'errors' => $user->getErrors()
];
}
public function discard($id)
{
parent::discard($id);
}
}

View File

@ -107,6 +107,13 @@ class AppController extends Controller
} else if ($this->ParamHandler->isRest()) {
throw new MethodNotAllowedException(__('Invalid user credentials.'));
}
// if ($this->request->getParam('action') === 'index') {
// $this->Security->setConfig('validatePost', false);
// }
$this->Security->setConfig('unlockedActions', ['index']);
$this->Security->setConfig('validatePost', false);
$this->ACL->checkAccess();
$this->set('menu', $this->ACL->getMenu());
$this->set('ajax', $this->request->is('ajax'));

View File

@ -642,6 +642,29 @@ class ACLComponent extends Component
]
]
],
'Inbox' => [
'label' => __('Inbox'),
'url' => '/inbox/index',
'children' => [
'index' => [
'url' => '/inbox/index',
'label' => __('Inbox')
],
'view' => [
'url' => '/inbox/view/{{id}}',
'label' => __('View Meta Template'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1
],
'delete' => [
'url' => '/inbox/delete/{{id}}',
'label' => __('Delete Meta Template'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1,
'popup' => 1
]
]
],
'MetaTemplates' => [
'label' => __('Meta Field Templates'),
'url' => '/metaTemplates/index',

View File

@ -145,7 +145,7 @@ class CRUDComponent extends Component
$message = __(
'{0} could not be added.{1}',
$this->ObjectAlias,
empty($validationMessage) ? '' : ' ' . __('Reason:{0}', $validationMessage)
empty($validationMessage) ? '' : PHP_EOL . __('Reason:{0}', $validationMessage)
);
if ($this->Controller->ParamHandler->isRest()) {
} else if ($this->Controller->ParamHandler->isAjax()) {
@ -485,7 +485,7 @@ class CRUDComponent extends Component
if (is_bool($contextFromField)) {
$contextFromFieldText = sprintf('%s: %s', $field, $contextFromField ? 'true' : 'false');
} else {
$contextFromFieldText = $contextFromField;
$contextFromFieldText = sprintf('%s: %s', $field, $contextFromField);
}
$filteringContexts[] = [
'label' => Inflector::humanize($contextFromFieldText),

View File

@ -423,11 +423,12 @@ class RestResponseComponent extends Component
public function ajaxSuccessResponse($ObjectAlias, $action, $entity, $message, $additionalData=[])
{
$action = $this->__dissectAdminRouting($action);
$entity = is_array($entity) ? $entity : $entity->toArray();
$response = [
'success' => true,
'message' => $message,
'data' => $entity->toArray(),
'url' => $this->__generateURL($action, $ObjectAlias, $entity->id)
'data' => $entity,
'url' => !empty($entity['id']) ? $this->__generateURL($action, $ObjectAlias, $entity['id']) : ''
];
if (!empty($additionalData)) {
$response['additionalData'] = $additionalData;

View File

@ -18,7 +18,7 @@ class EncryptionKeysController extends AppController
{
$this->CRUD->index([
'quickFilters' => ['encryption_key'],
'filters' => ['owner_type', 'organisation_id', 'individual_id', 'encryption_key'],
'filters' => ['owner_model', 'organisation_id', 'individual_id', 'encryption_key'],
'contextFilters' => [
'fields' => [
'type'

View File

@ -0,0 +1,101 @@
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use Cake\Database\Expression\QueryExpression;
use Cake\Event\EventInterface;
use Cake\Http\Exception\NotFoundException;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\ForbiddenException;
class InboxController extends AppController
{
public $filters = ['scope', 'action', 'title', 'origin', 'comment'];
public function beforeFilter(EventInterface $event)
{
parent::beforeFilter($event);
$this->set('metaGroup', 'Administration');
}
public function index()
{
$this->CRUD->index([
'filters' => $this->filters,
'quickFilters' => ['scope', 'action', ['title' => true], ['comment' => true]],
'contextFilters' => [
'fields' => [
'scope',
'action',
]
],
'contain' => ['Users']
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function filtering()
{
$this->CRUD->filtering();
}
// public function add()
// {
// $this->CRUD->add();
// $responsePayload = $this->CRUD->getResponsePayload();
// if (!empty($responsePayload)) {
// return $responsePayload;
// }
// }
public function view($id)
{
$this->CRUD->view($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function delete($id)
{
$this->CRUD->delete($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function process($id)
{
$request = $this->Inbox->get($id);
$scope = $request->scope;
$action = $request->action;
$processor = $this->Inbox->getRequestProcessor($scope, $action);
if ($this->request->is('post')) {
$processResult = $processor->process($id, $this->request);
if ($processResult['success']) {
$message = !empty($processResult['message']) ? $processResult['message'] : __('Request {0} processed.', $id);
$response = $this->RestResponse->ajaxSuccessResponse('RequestProcessor', "{$scope}.{$action}", $processResult['data'], $message);
} else {
$message = !empty($processResult['message']) ? $processResult['message'] : __('Request {0} could not be processed.', $id);
$response = $this->RestResponse->ajaxFailResponse('RequestProcessor', "{$scope}.{$action}", $processResult['data'], $message, $processResult['errors']);
}
return $response;
} else {
$processor->setViewVariables($this, $request);
$processingTemplate = $processor->getProcessingTemplate();
$this->set('request', $request);
$this->viewBuilder()->setLayout('ajax');
$this->render($processingTemplate);
}
}
}

View File

@ -4,6 +4,7 @@ namespace App\Controller;
use App\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use Cake\ORM\TableRegistry;
use \Cake\Database\Expression\QueryExpression;
class UsersController extends AppController
@ -134,4 +135,23 @@ class UsersController extends AppController
return $this->redirect(['controller' => 'Users', 'action' => 'login']);
}
}
public function register()
{
$this->Inbox = TableRegistry::getTableLocator()->get('Inbox');
$processor = $this->Inbox->getRequestProcessor('User', 'Registration');
$data = [
'origin' => '127.0.0.1',
'comment' => 'Hi there!, please create an account',
'data' => [
'username' => 'foobar',
'email' => 'foobar@admin.test',
'first_name' => 'foo',
'last_name' => 'bar',
],
];
$processor->create($data);
$this->Flash->success(__('Entry created'));
return $this->redirect(['controller' => 'Inbox', 'action' => 'index']);
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
class Inbox extends AppModel
{
}

View File

@ -18,14 +18,14 @@ class EncryptionKeysTable extends AppTable
'Individuals',
[
'foreignKey' => 'owner_id',
'conditions' => ['owner_type' => 'individual']
'conditions' => ['owner_model' => 'individual']
]
);
$this->belongsTo(
'Organisations',
[
'foreignKey' => 'owner_id',
'conditions' => ['owner_type' => 'organisation']
'conditions' => ['owner_model' => 'organisation']
]
);
$this->setDisplayField('encryption_key');
@ -34,13 +34,13 @@ class EncryptionKeysTable extends AppTable
public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
{
if (empty($data['owner_id'])) {
if (empty($data['owner_type'])) {
if (empty($data['owner_model'])) {
return false;
}
if (empty($data[$data['owner_type'] . '_id'])) {
if (empty($data[$data['owner_model'] . '_id'])) {
return false;
}
$data['owner_id'] = $data[$data['owner_type'] . '_id'];
$data['owner_id'] = $data[$data['owner_model'] . '_id'];
}
}
@ -50,8 +50,8 @@ class EncryptionKeysTable extends AppTable
->notEmptyString('type')
->notEmptyString('encryption_key')
->notEmptyString('owner_id')
->notEmptyString('owner_type')
->requirePresence(['type', 'encryption_key', 'owner_id', 'owner_type'], 'create');
->notEmptyString('owner_model')
->requirePresence(['type', 'encryption_key', 'owner_id', 'owner_model'], 'create');
return $validator;
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\Database\Schema\TableSchemaInterface;
use Cake\Database\Type;
use Cake\Filesystem\Folder;
use Cake\ORM\Table;
use Cake\ORM\RulesChecker;
use Cake\Validation\Validator;
Type::map('json', 'Cake\Database\Type\JsonType');
class InboxTable extends AppTable
{
private $processorsDirectory = ROOT . '/libraries/RequestProcessors';
private $requestProcessors;
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp', [
'events' => [
'Model.beforeSave' => [
'created' => 'new'
]
]
]);
$this->belongsTo('Users');
$this->setDisplayField('title');
}
protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaInterface
{
$schema->setColumnType('data', 'json');
return $schema;
}
public function validationDefault(Validator $validator): Validator
{
$validator
->notEmptyString('scope')
->notEmptyString('action')
->notEmptyString('title')
->notEmptyString('origin')
->datetime('created')
->requirePresence([
'scope' => ['message' => __('The field `scope` is required')],
'action' => ['message' => __('The field `action` is required')],
'title' => ['message' => __('The field `title` is required')],
'origin' => ['message' => __('The field `origin` is required')],
], 'create');
return $validator;
}
public function buildRules(RulesChecker $rules): RulesChecker
{
$rules->add($rules->existsIn('user_id', 'Users'), [
'message' => 'The provided `user_id` does not exist'
]);
return $rules;
}
public function getRequestProcessor($name, $action=null)
{
if (!isset($this->requestProcessors)) {
$this->loadRequestProcessors();
}
if (isset($this->requestProcessors[$name])) {
if (is_null($action)) {
return $this->requestProcessors[$name];
} else if (!empty($this->requestProcessors[$name]->{$action})) {
return $this->requestProcessors[$name]->{$action};
} else {
throw new \Exception(__('Processor {0}.{1} not found', $name, $action));
}
}
throw new \Exception(__('Processor not found'), 1);
}
private function loadRequestProcessors()
{
$processorDir = new Folder($this->processorsDirectory);
$processorFiles = $processorDir->find('.*RequestProcessor\.php', true);
foreach ($processorFiles as $processorFile) {
if ($processorFile == 'GenericRequestProcessor.php') {
continue;
}
$processorMainClassName = str_replace('.php', '', $processorFile);
$processorMainClassNameShort = str_replace('RequestProcessor.php', '', $processorFile);
$processorMainClass = $this->getProcessorClass($processorDir->pwd() . DS . $processorFile, $processorMainClassName);
if ($processorMainClass !== false) {
$this->requestProcessors[$processorMainClassNameShort] = $processorMainClass;
}
}
}
private function getProcessorClass($filePath, $processorMainClassName)
{
require_once($filePath);
$reflection = new \ReflectionClass($processorMainClassName);
$processorMainClass = $reflection->newInstance(true);
if ($processorMainClass->checkLoading() === 'Assimilation successful!') {
return $processorMainClass;
}
try {
} catch (Exception $e) {
return false;
}
}
}

View File

@ -25,7 +25,7 @@ class IndividualsTable extends AppTable
'EncryptionKeys',
[
'foreignKey' => 'owner_id',
'conditions' => ['owner_type' => 'individual']
'conditions' => ['owner_model' => 'individual']
]
);
$this->hasOne(

View File

@ -30,7 +30,7 @@ class OrganisationsTable extends AppTable
[
'dependent' => true,
'foreignKey' => 'owner_id',
'conditions' => ['owner_type' => 'organisation']
'conditions' => ['owner_model' => 'organisation']
]
);
$this->hasMany(

View File

@ -822,7 +822,7 @@ class BoostrapModal extends BootstrapGeneric {
'variant' => 'primary',
'text' => __('Ok'),
'params' => [
'data-dismiss' => 'modal',
'data-dismiss' => $this->options['confirmFunction'] ? '' : 'modal',
'onclick' => $this->options['confirmFunction']
]
]))->button();
@ -848,8 +848,9 @@ class BoostrapModal extends BootstrapGeneric {
'variant' => $variant,
'text' => h($this->options['confirmText']),
'params' => [
'data-dismiss' => 'modal',
'onclick' => $this->options['confirmFunction']
'data-dismiss' => $this->options['confirmFunction'] ? '' : 'modal',
// 'onclick' => sprintf('(function(clicked) { %s.finally( () => { $(clicked).closest(\'.modal\').data(\'modalObject\').hide() }) }(this))', $this->options['confirmFunction'])
'onclick' => sprintf('closeModalOnFunctionCompletion(this, function(clicked) { return %s })', $this->options['confirmFunction'])
]
]))->button();
return $buttonCancel . $buttonConfirm;

View File

@ -6,7 +6,7 @@ echo $this->element('genericElements/Form/genericForm', [
'model' => 'Organisations',
'fields' => [
[
'field' => 'owner_type',
'field' => 'owner_model',
'label' => __('Owner type'),
'options' => array_combine(array_keys($dropdownData), array_keys($dropdownData)),
'type' => 'dropdown'
@ -17,7 +17,7 @@ echo $this->element('genericElements/Form/genericForm', [
'options' => $dropdownData['organisation'] ?? [],
'type' => 'dropdown',
'stateDependence' => [
'source' => '#owner_type-field',
'source' => '#owner_model-field',
'option' => 'organisation'
]
],
@ -27,7 +27,7 @@ echo $this->element('genericElements/Form/genericForm', [
'options' => $dropdownData['individual'] ?? [],
'type' => 'dropdown',
'stateDependence' => [
'source' => '#owner_type-field',
'source' => '#owner_model-field',
'option' => 'individual'
]
],

View File

@ -45,7 +45,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
[
'name' => __('Owner'),
'data_path' => 'owner_id',
'owner_type_path' => 'owner_type',
'owner_model_path' => 'owner_model',
'element' => 'owner'
],
[

91
templates/Inbox/index.php Normal file
View File

@ -0,0 +1,91 @@
<?php
echo $this->Html->scriptBlock(sprintf(
'var csrfToken = %s;',
json_encode($this->request->getAttribute('csrfToken'))
));
echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'data' => $data,
'top_bar' => [
'children' => [
[
'type' => 'context_filters',
'context_filters' => !empty($filteringContexts) ? $filteringContexts : []
],
[
'type' => 'search',
'button' => __('Filter'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
'allowFilering' => true
]
]
],
'fields' => [
[
'name' => '#',
'sort' => 'id',
'data_path' => 'id',
],
[
'name' => 'created',
'sort' => 'created',
'data_path' => 'created',
'element' => 'datetime'
],
[
'name' => 'scope',
'sort' => 'scope',
'data_path' => 'scope',
],
[
'name' => 'action',
'sort' => 'action',
'data_path' => 'action',
],
[
'name' => 'title',
'sort' => 'title',
'data_path' => 'title',
],
[
'name' => 'origin',
'sort' => 'origin',
'data_path' => 'origin',
],
[
'name' => 'user',
'sort' => 'user_id',
'data_path' => 'user',
'element' => 'user'
],
[
'name' => 'description',
'sort' => 'description',
'data_path' => 'description',
],
[
'name' => 'comment',
'sort' => 'comment',
'data_path' => 'comment',
],
],
'title' => __('Inbox'),
'description' => __('A list of requests to be manually processed'),
'actions' => [
[
'open_modal' => '/inbox/process/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'eye'
],
[
'open_modal' => '/individuals/delete/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'trash'
],
]
]
]);
echo '</div>';
?>

50
templates/Inbox/view.php Normal file
View File

@ -0,0 +1,50 @@
<?php
echo $this->element(
'/genericElements/SingleViews/single_view',
[
'data' => $entity,
'fields' => [
[
'key' => __('ID'),
'path' => 'id'
],
[
'key' => 'created',
'path' => 'created',
],
[
'key' => 'scope',
'path' => 'scope',
],
[
'key' => 'action',
'path' => 'action',
],
[
'key' => 'title',
'path' => 'title',
],
[
'key' => 'origin',
'path' => 'origin',
],
[
'key' => 'user_id',
'path' => 'user_id',
],
[
'key' => 'description',
'path' => 'description',
],
[
'key' => 'comment',
'path' => 'comment',
],
[
'key' => 'data',
'path' => 'data',
],
],
'children' => []
]
);

View File

@ -51,7 +51,7 @@ function runAllUpdate() {
type: 'confirm-success',
confirmText: '<?= __n('Run update', 'Run all updates', count($updateAvailables)) ?>',
APIConfirm: (tmpApi) => {
tmpApi.fetchAndPostForm(url, {}).then(() => {
return tmpApi.fetchAndPostForm(url, {}).then(() => {
location.reload()
})
},

View File

@ -0,0 +1,122 @@
<?php
$formIndividual = $this->element('genericElements/Form/genericForm', [
'entity' => $individualEntity,
'ajax' => false,
'raw' => true,
'data' => [
'description' => __('Create individual'),
'model' => 'Individual',
'fields' => [
[
'field' => 'email',
'autocomplete' => 'off'
],
[
'field' => 'uuid',
'label' => 'UUID',
'type' => 'uuid',
'autocomplete' => 'off'
],
[
'field' => 'first_name',
'autocomplete' => 'off'
],
[
'field' => 'last_name',
'autocomplete' => 'off'
],
[
'field' => 'position',
'autocomplete' => 'off'
],
],
'submit' => [
'action' => $this->request->getParam('action')
]
]
]);
$formUser = $this->element('genericElements/Form/genericForm', [
'entity' => $userEntity,
'ajax' => false,
'raw' => true,
'data' => [
'description' => __('Create user account'),
'model' => 'User',
'fields' => [
[
'field' => 'individual_id',
'type' => 'dropdown',
'label' => __('Associated individual'),
'options' => $dropdownData['individual'],
],
[
'field' => 'username',
'autocomplete' => 'off',
],
[
'field' => 'role_id',
'type' => 'dropdown',
'label' => __('Role'),
'options' => $dropdownData['role']
],
[
'field' => 'disabled',
'type' => 'checkbox',
'label' => 'Disable'
]
],
'submit' => [
'action' => $this->request->getParam('action')
]
]
]);
echo $this->Bootstrap->modal([
'title' => __('Register user'),
'size' => 'lg',
'type' => 'confirm',
'bodyHtml' => sprintf('<div class="individual-container">%s</div><div class="user-container">%s</div>',
$formIndividual,
$formUser
),
'confirmText' => __('Submit'),
'confirmFunction' => 'submitRegistration(clicked)'
]);
?>
</div>
<script>
function submitRegistration(clicked) {
const tmpApi = new AJAXApi({
statusNode: clicked
})
const $forms = $(clicked).closest('.modal').find('form')
const url = $forms[0].action
const data1 = getFormData($forms[0])
const data2 = getFormData($forms[1])
const data = {...data1, ...data2}
return tmpApi.postData(url, data)
}
$(document).ready(function() {
$('div.user-container #individual_id-field').change(function() {
if ($(this).val() == -1) {
$('div.individual-container').show()
} else {
$('div.individual-container').hide()
}
})
})
function getFormData(form) {
return Object.values(form).reduce((obj,field) => {
if (field.type === 'checkbox') {
obj[field.name] = field.checked;
} else {
obj[field.name] = field.value;
}
return obj
}, {})
}
</script>

View File

@ -115,6 +115,24 @@
'actionButton' => $this->element('genericElements/Form/submitButton', $submitButtonData),
'class' => 'modal-lg'
]);
} else if (!empty($raw)) {
echo sprintf(
'%s%s%s%s%s%s',
empty($data['description']) ? '' : sprintf(
'<div class="pb-2">%s</div>',
$data['description']
),
$ajaxFlashMessage,
$formCreate,
$fieldsString,
empty($metaTemplateString) ? '' : $this->element(
'genericElements/accordion_scaffold', [
'body' => $metaTemplateString,
'title' => 'Meta fields'
]
),
$formEnd
);
} else {
echo sprintf(
'%s<h2>%s</h2>%s%s%s%s%s%s%s%s%s',

View File

@ -1,5 +1,5 @@
<?php
$type = $this->Hash->extract($row, $field['owner_type_path'])[0];
$type = $this->Hash->extract($row, $field['owner_model_path'])[0];
$owner = $row[$type];
$types = [
'individual' => [

View File

@ -0,0 +1,11 @@
<?php
if (!empty($row['user'])) {
$userId = $this->Hash->extract($row, 'user.id')[0];
$userName = $this->Hash->extract($row, 'user.username')[0];
echo $this->Html->link(
h($userName),
['controller' => 'users', 'action' => 'view', $userId]
);
}
?>

View File

@ -12,6 +12,7 @@
* - id: element ID for the input field - defaults to quickFilterField
*/
if (!isset($data['requirement']) || $data['requirement']) {
$filterEffective = !empty($quickFilter); // No filters will be picked up, thus rendering the filtering useless
$filteringButton = '';
if (!empty($data['allowFilering'])) {
$activeFilters = !empty($activeFilters) ? $activeFilters : [];
@ -32,9 +33,10 @@
$filteringButton = $this->Bootstrap->button($buttonConfig);
}
$button = empty($data['button']) && empty($data['fa-icon']) ? '' : sprintf(
'<div class="input-group-append"><button class="btn btn-primary" %s id="quickFilterButton-%s">%s%s</button>%s</div>',
'<div class="input-group-append"><button class="btn btn-primary" %s id="quickFilterButton-%s" %s>%s%s</button>%s</div>',
empty($data['data']) ? '' : h($data['data']),
h($tableRandomValue),
$filterEffective ? '' : 'disabled="disabled"',
empty($data['fa-icon']) ? '' : sprintf('<i class="fa fa-%s"></i>', h($data['fa-icon'])),
empty($data['button']) ? '' : h($data['button']),
$filteringButton
@ -43,13 +45,14 @@
$button .= $this->element('/genericElements/ListTopBar/element_simple', array('data' => $data['cancel']));
}
$input = sprintf(
'<input id="quickFilterField-%s" type="text" class="form-control" placeholder="%s" aria-label="%s" style="padding: 2px 6px;" id="%s" data-searchkey="%s" value="%s">',
'<input id="quickFilterField-%s" type="text" class="form-control" placeholder="%s" aria-label="%s" style="padding: 2px 6px;" id="%s" data-searchkey="%s" value="%s" %s>',
h($tableRandomValue),
empty($data['placeholder']) ? '' : h($data['placeholder']),
empty($data['placeholder']) ? '' : h($data['placeholder']),
empty($data['id']) ? 'quickFilterField' : h($data['id']),
empty($data['searchKey']) ? 'searchall' : h($data['searchKey']),
empty($data['value']) ? (!empty($quickFilterValue) ? h($quickFilterValue) : '') : h($data['value'])
empty($data['value']) ? (!empty($quickFilterValue) ? h($quickFilterValue) : '') : h($data['value']),
$filterEffective ? '' : 'disabled="disabled"'
);
echo sprintf(
'<div class="input-group" data-table-random-value="%s" style="margin-left: auto;">%s%s</div>',

View File

@ -35,7 +35,7 @@ if (isset($menu[$metaGroup])) {
}
$active = ($scope === $this->request->getParam('controller') && $action === $this->request->getParam('action'));
if (!empty($data['popup'])) {
$link_template = '<a href="#" onClick="UI.submissionModalForSinglePage(\'%s\')" class="list-group-item list-group-item-action %s %s pl-3 border-0 %s">%s</a>';
$link_template = '<a href="#" onClick="UI.submissionModalAutoGuess(\'%s\')" class="list-group-item list-group-item-action %s %s pl-3 border-0 %s">%s</a>';
} else {
$link_template = '<a href="%s" class="list-group-item list-group-item-action %s %s pl-3 border-0 %s">%s</a>';
}

View File

@ -17,7 +17,7 @@
<?= $this->Form->postLink(
'Delete',
(empty($postLinkParameters) ? ['action' => 'delete', $id] : $postLinkParameters),
['class' => 'btn btn-primary button-execute', 'id' => 'submitButton']
['class' => 'btn btn-danger button-execute', 'id' => 'submitButton']
)
?>
<button type="button" class="btn btn-secondary cancel-button" data-dismiss="modal"><?= __('Cancel') ?></button>

View File

@ -30,7 +30,7 @@ echo $this->Bootstrap->modal([
'type' => 'confirm',
'bodyHtml' => $filteringForm,
'confirmText' => __('Filter'),
'confirmFunction' => 'filterIndex(this)'
'confirmFunction' => 'filterIndex(clicked)'
]);
?>

View File

@ -286,6 +286,7 @@ class AJAXApi {
let formData = new FormData()
formData = AJAXApi.mergeFormData(formData, dataToPost)
let requestConfig = AJAXApi.genericRequestConfigPOST
requestConfig.headers.append('AUTHORIZATION', '~HACKY-HACK~')
let options = {
...requestConfig,
body: formData,
@ -307,8 +308,6 @@ class AJAXApi {
title: 'There has been a problem with the operation',
body: data.message
}, true, skipFeedback);
feedbackShown = true
this.injectFormValidationFeedback(form, data.errors)
toReturn = Promise.reject(data.errors);
}
} catch (error) {

View File

@ -131,6 +131,35 @@ class UIFactory {
return UI.submissionReloaderModal(url, reloadUrl, $reloadedElement, $statusNode);
}
/**
* Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the index table after a successful operation.
* Supports `displayOnSuccess` option to show another modal after the submission
* @param {string} url - The URL from which the modal's content should be fetched
* @param {(boolean|string)} [reloadUrl=false] - The URL from which the data should be fetched after confirming
* @param {(jQuery|string)} [$table=false] - The table ID which should be reloaded on success
* @return {Promise<Object>} Promise object resolving to the ModalFactory object
*/
submissionModalAutoGuess(url, reloadUrl=false, $table=false) {
let currentAction = location.pathname.split('/')[2]
currentAction += 'cdsc'
if (currentAction !== undefined) {
if (currentAction === 'index') {
return UI.submissionModalForIndex(url, reloadUrl, $table)
} else if (currentAction === 'view') {
return UI.submissionModalForSinglePage(url, reloadUrl, $table)
}
}
const successCallback = () => {
UI.toast({
variant: 'danger',
title: 'Could not reload the page',
body: 'Reloading the page manually is advised.'
})
}
return UI.submissionModal(url, successCallback)
}
/**
* Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the provided element after a successful operation.
* Supports `displayOnSuccess` option to show another modal after the submission
@ -322,7 +351,7 @@ class Toaster {
if (options.bodyHtml !== false) {
$toastBody = $('<div class="toast-body"/>').html(options.bodyHtml)
} else {
$toastBody = $('<div class="toast-body"/>').text(options.body)
$toastBody = $('<div class="toast-body"/>').append($('<div style="white-space: break-spaces;"/>').text(options.body))
}
$toast.append($toastBody)
}

View File

@ -66,6 +66,17 @@ function attachTestConnectionResultHtml(result, $container) {
return $testResultDiv
}
function closeModalOnFunctionCompletion(clicked, fun) {
const result = fun(clicked)
if (result === undefined) {
$(clicked).closest('.modal').data('modalObject').hide()
} else {
result.finally( () => {
$(clicked).closest('.modal').data('modalObject').hide()
})
}
}
var UI
$(document).ready(() => {
if (typeof UIFactory !== "undefined") {