commit
a7dca8284b
|
@ -28,7 +28,7 @@ server {
|
|||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/var/run/php/php-fpm.sock;
|
||||
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Migrations\AbstractMigration;
|
||||
use Phinx\Db\Adapter\MysqlAdapter;
|
||||
|
||||
final class MoreDataOnMetaFields extends AbstractMigration
|
||||
{
|
||||
public $autoId = false; // turn off automatic `id` column create. We want it to be `int(10) unsigned`
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$metaFieldTable = $this->table('meta_fields');
|
||||
if (!$metaFieldTable->hasColumn('meta_template_directory_id')) {
|
||||
$metaFieldTable
|
||||
->addColumn('meta_template_directory_id', 'integer', [
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
'signed' => false,
|
||||
'length' => 10
|
||||
])
|
||||
->addIndex('meta_template_directory_id')
|
||||
->update();
|
||||
}
|
||||
|
||||
$exists = $this->hasTable('meta_template_name_directory');
|
||||
if (!$exists) {
|
||||
$templateNameDirectoryTable = $this->table('meta_template_name_directory', [
|
||||
'signed' => false,
|
||||
'collation' => 'utf8mb4_unicode_ci'
|
||||
]);
|
||||
$templateNameDirectoryTable
|
||||
->addColumn('id', 'integer', [
|
||||
'autoIncrement' => true,
|
||||
'limit' => 10,
|
||||
'signed' => false,
|
||||
])
|
||||
->addPrimaryKey('id')
|
||||
->addColumn('name', 'string', [
|
||||
'null' => false,
|
||||
'limit' => 191,
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'encoding' => 'utf8mb4',
|
||||
])
|
||||
->addColumn('namespace', 'string', [
|
||||
'null' => false,
|
||||
'limit' => 191,
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'encoding' => 'utf8mb4',
|
||||
])
|
||||
->addColumn('version', 'string', [
|
||||
'null' => false,
|
||||
'limit' => 191,
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'encoding' => 'utf8mb4',
|
||||
])
|
||||
->addColumn('uuid', 'uuid', [
|
||||
'null' => false,
|
||||
'default' => null,
|
||||
]);
|
||||
|
||||
$templateNameDirectoryTable
|
||||
->addIndex(['uuid', 'version'], ['unique' => true])
|
||||
->addIndex('name')
|
||||
->addIndex('namespace');
|
||||
|
||||
$templateNameDirectoryTable->create();
|
||||
|
||||
$allTemplates = $this->getAllTemplates();
|
||||
$this->populateTemplateDirectoryTable($allTemplates);
|
||||
|
||||
$metaTemplateTable = $this->table('meta_templates');
|
||||
$metaTemplateTable
|
||||
->addColumn('meta_template_directory_id', 'integer', [
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
'signed' => false,
|
||||
'length' => 10
|
||||
])
|
||||
->update();
|
||||
$this->assignTemplateDirectory($allTemplates);
|
||||
$metaTemplateTable
|
||||
->addForeignKey('meta_template_directory_id', 'meta_template_name_directory', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->save();
|
||||
|
||||
$metaFieldTable
|
||||
->dropForeignKey('meta_template_id')
|
||||
->dropForeignKey('meta_template_field_id')
|
||||
->addForeignKey('meta_template_directory_id', 'meta_template_name_directory', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function populateTemplateDirectoryTable(array $allTemplates): void
|
||||
{
|
||||
$builder = $this->getQueryBuilder()
|
||||
->insert(['uuid', 'name', 'namespace', 'version'])
|
||||
->into('meta_template_name_directory');
|
||||
|
||||
if (!empty($allTemplates)) {
|
||||
foreach ($allTemplates as $template) {
|
||||
$builder->values([
|
||||
'uuid' => $template['uuid'],
|
||||
'name' => $template['name'],
|
||||
'namespace' => $template['namespace'],
|
||||
'version' => $template['version'],
|
||||
]);
|
||||
}
|
||||
$builder->execute();
|
||||
}
|
||||
}
|
||||
|
||||
private function assignTemplateDirectory(array $allTemplates): void
|
||||
{
|
||||
foreach ($allTemplates as $template) {
|
||||
$directory_template = $this->getDirectoryTemplate($template['uuid'], $template['version'])[0];
|
||||
$this->getQueryBuilder()
|
||||
->update('meta_templates')
|
||||
->set('meta_template_directory_id', $directory_template['id'])
|
||||
->where(['meta_template_id' => $template['id']])
|
||||
->execute();
|
||||
$this->getQueryBuilder()
|
||||
->update('meta_fields')
|
||||
->set('meta_template_directory_id', $directory_template['id'])
|
||||
->where(['id' => $template['id']])
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
private function getAllTemplates(): array
|
||||
{
|
||||
return $this->getQueryBuilder()
|
||||
->select(['id', 'uuid', 'name', 'namespace', 'version'])
|
||||
->from('meta_templates')
|
||||
->execute()->fetchAll('assoc');
|
||||
}
|
||||
|
||||
private function getDirectoryTemplate(string $uuid, string $version): array
|
||||
{
|
||||
return $this->getQueryBuilder()
|
||||
->select(['id', 'uuid', 'version'])
|
||||
->from('meta_template_name_directory')
|
||||
->where([
|
||||
'uuid' => $uuid,
|
||||
'version' => $version,
|
||||
])
|
||||
->execute()->fetchAll('assoc');
|
||||
}
|
||||
}
|
|
@ -50,28 +50,28 @@ $footerButtons[] = [
|
|||
|
||||
$table = $this->Bootstrap->table(['small' => true, 'bordered' => false, 'striped' => false, 'hover' => false], [
|
||||
'fields' => [
|
||||
['key' => 'created', 'label' => __('Date'), 'formatter' => function($value, $row) {
|
||||
['path' => 'created', 'label' => __('Date'), 'formatter' => function($value, $row) {
|
||||
return $value->i18nFormat('yyyy-MM-dd HH:mm:ss');
|
||||
}],
|
||||
['key' => 'connector', 'label' => __('Tool Name'), 'formatter' => function($connector, $row) {
|
||||
['path' => 'connector', 'label' => __('Tool Name'), 'formatter' => function($connector, $row) {
|
||||
return sprintf('<a href="%s" target="_blank">%s</a>',
|
||||
$this->Url->build(['controller' => 'localTools', 'action' => 'viewConnector', $connector['name']]),
|
||||
sprintf('%s (v%s)', h($connector['name']), h($connector['connector_version']))
|
||||
);
|
||||
}],
|
||||
['key' => 'brood', 'label' => __('Brood'), 'formatter' => function($brood, $row) {
|
||||
['path' => 'brood', 'label' => __('Brood'), 'formatter' => function($brood, $row) {
|
||||
return sprintf('<a href="%s" target="_blank">%s</a>',
|
||||
$this->Url->build(['controller' => 'broods', 'action' => 'view', $brood['id']]),
|
||||
h($brood['name'])
|
||||
);
|
||||
}],
|
||||
['key' => 'individual', 'label' => __('Individual'), 'formatter' => function($individual, $row) {
|
||||
['path' => 'individual', 'label' => __('Individual'), 'formatter' => function($individual, $row) {
|
||||
return sprintf('<a href="%s" target="_blank">%s</a>',
|
||||
$this->Url->build(['controller' => 'users', 'action' => 'view', $individual['id']]),
|
||||
h($individual['email'])
|
||||
);
|
||||
}],
|
||||
['key' => 'individual.alignments', 'label' => __('Alignment'), 'formatter' => function($alignments, $row) {
|
||||
['path' => 'individual.alignments', 'label' => __('Alignment'), 'formatter' => function($alignments, $row) {
|
||||
$html = '';
|
||||
foreach ($alignments as $alignment) {
|
||||
$html .= sprintf('<div class="text-nowrap"><b>%s</b> @ <a href="%s" target="_blank">%s</a></div>',
|
||||
|
@ -101,7 +101,7 @@ $localToolHTML = $this->fetch('content', sprintf('<div class="d-none">%s</div><d
|
|||
|
||||
$requestData = $this->Bootstrap->collapse(
|
||||
[
|
||||
'title' => __('Inter-connection data'),
|
||||
'text' => __('Inter-connection data'),
|
||||
'open' => true,
|
||||
],
|
||||
sprintf('<pre class="p-2 rounded mb-0" style="background: #eeeeee55;"><code>%s</code></pre>', json_encode($request['data'], JSON_PRETTY_PRINT))
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
$randomIdOld = Cake\Utility\Security::randomString(8);
|
||||
$randomIdNew = Cake\Utility\Security::randomString(8);
|
||||
|
||||
if (!empty($data['summary'])) {
|
||||
$changedSummary = h($data['summary']);
|
||||
} else if (!empty($data['summaryTemplate']) && !empty($data['summaryMessage'])) {
|
||||
|
@ -28,32 +31,126 @@ $form = $this->element('genericElements/Form/genericForm', [
|
|||
]
|
||||
]);
|
||||
|
||||
$properties = array_unique(array_merge(array_keys($data['original']), array_keys($data['changed'])));
|
||||
$tableData = [];
|
||||
foreach ($properties as $i => $property) {
|
||||
$tableData[] = [
|
||||
$property,
|
||||
$data['original'][$property] ?? '',
|
||||
$data['changed'][$property] ?? '',
|
||||
];
|
||||
}
|
||||
$emptyValueHTML = $this->Bootstrap->node('span', ['class' => ['text-muted', 'fw-light', 'fst-italic']], __('- empty -'));
|
||||
|
||||
$diffTable = $this->Bootstrap->table(
|
||||
[
|
||||
'hover' => false,
|
||||
'striped' => false,
|
||||
'bordered' => false,
|
||||
],
|
||||
[
|
||||
'items' => $tableData,
|
||||
'fields' => [
|
||||
[
|
||||
'label' => __('Property name'),
|
||||
'formatter' => function ($field, $row) {
|
||||
return $this->Bootstrap->node('pre', [], h($field));
|
||||
}
|
||||
],
|
||||
[
|
||||
'label' => __('Old value'),
|
||||
'formatter' => function ($field, $row) use ($randomIdOld, $emptyValueHTML) {
|
||||
$fieldText = is_array($field) ? json_encode($field, JSON_FORCE_OBJECT | JSON_PRETTY_PRINT) : $field;
|
||||
$config = [
|
||||
'text' => $fieldText,
|
||||
'variant' => 'danger',
|
||||
'dismissible' => false,
|
||||
'class' => ['p-2', 'mb-0', !empty($fieldText) && is_array($field) ? "json_container_{$randomIdOld}" : ''],
|
||||
];
|
||||
if (empty($fieldText)) {
|
||||
$config['html'] = $emptyValueHTML;
|
||||
} else {
|
||||
$config['text'] = $fieldText;
|
||||
}
|
||||
return $this->Bootstrap->alert($config);
|
||||
}
|
||||
],
|
||||
[
|
||||
'label' => __('New value'),
|
||||
'formatter' => function ($field, $row) use ($randomIdNew, $emptyValueHTML) {
|
||||
$fieldText = is_array($field) ? json_encode($field, JSON_FORCE_OBJECT | JSON_PRETTY_PRINT) : $field;
|
||||
$config = [
|
||||
'text' => $fieldText,
|
||||
'variant' => 'success',
|
||||
'dismissible' => false,
|
||||
'class' => ['p-2', 'mb-0', !empty($fieldText) && is_array($field) ? "json_container_{$randomIdNew}" : ''],
|
||||
];
|
||||
if (empty($fieldText)) {
|
||||
$config['html'] = $emptyValueHTML;
|
||||
} else {
|
||||
$config['text'] = $fieldText;
|
||||
}
|
||||
return $this->Bootstrap->alert($config);
|
||||
}
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
$cards = sprintf(
|
||||
'<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">%s</div>
|
||||
<div class="col">%s</div>
|
||||
</div>
|
||||
</div>',
|
||||
$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']]])
|
||||
])
|
||||
);
|
||||
|
||||
$collapse = $this->Bootstrap->collapse([
|
||||
'button' => [
|
||||
'text' => __('Show raw changes'),
|
||||
'variant' => 'link',
|
||||
],
|
||||
'card' => false
|
||||
], $cards);
|
||||
|
||||
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>',
|
||||
'<div class="d-none">%s</div><p>%s</p>%s%s',
|
||||
$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']]])
|
||||
])
|
||||
$diffTable,
|
||||
$collapse
|
||||
),
|
||||
'confirmText' => __('Acknowledge & Discard'),
|
||||
'confirmIcon' => 'check',
|
||||
'confirmButton' => [
|
||||
'text' => __('Acknowledge & Discard'),
|
||||
'icon' => 'check',
|
||||
]
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
const $containerOld = $('.json_container_<?= $randomIdOld; ?>')
|
||||
const $containerNew = $('.json_container_<?= $randomIdNew; ?>')
|
||||
if ($containerOld.length == 1) {
|
||||
$containerOld.html(syntaxHighlightJson($containerOld.text()));
|
||||
}
|
||||
if ($containerNew.length == 1) {
|
||||
$containerNew.html(syntaxHighlightJson($containerNew.text()));
|
||||
}
|
||||
});
|
||||
</script>
|
|
@ -40,22 +40,22 @@ $tools = sprintf(
|
|||
|
||||
$table = $this->Bootstrap->table(['small' => true, 'bordered' => false, 'striped' => false, 'hover' => false], [
|
||||
'fields' => [
|
||||
['key' => 'created', 'label' => __('Date'), 'formatter' => function($value, $row) {
|
||||
['path' => 'created', 'label' => __('Date'), 'formatter' => function($value, $row) {
|
||||
return $value->i18nFormat('yyyy-MM-dd HH:mm:ss');
|
||||
}],
|
||||
['key' => 'brood', 'label' => __('Brood'), 'formatter' => function($brood, $row) {
|
||||
['path' => 'brood', 'label' => __('Brood'), 'formatter' => function($brood, $row) {
|
||||
return sprintf('<a href="%s" target="_blank">%s</a>',
|
||||
$this->Url->build(['controller' => 'broods', 'action' => 'view', $brood['id']]),
|
||||
h($brood['name'])
|
||||
);
|
||||
}],
|
||||
['key' => 'individual', 'label' => __('Individual'), 'formatter' => function($individual, $row) {
|
||||
['path' => 'individual', 'label' => __('Individual'), 'formatter' => function($individual, $row) {
|
||||
return sprintf('<a href="%s" target="_blank">%s</a>',
|
||||
$this->Url->build(['controller' => 'users', 'action' => 'view', $individual['id']]),
|
||||
h($individual['email'])
|
||||
);
|
||||
}],
|
||||
['key' => 'individual.alignments', 'label' => __('Alignment'), 'formatter' => function($alignments, $row) {
|
||||
['path' => 'individual.alignments', 'label' => __('Alignment'), 'formatter' => function($alignments, $row) {
|
||||
$html = '';
|
||||
foreach ($alignments as $alignment) {
|
||||
$html .= sprintf('<div class="text-nowrap"><b>%s</b> @ <a href="%s" target="_blank">%s</a></div>',
|
||||
|
@ -71,7 +71,7 @@ $table = $this->Bootstrap->table(['small' => true, 'bordered' => false, 'striped
|
|||
]);
|
||||
|
||||
$requestData = $this->Bootstrap->collapse([
|
||||
'title' => __('Message data'),
|
||||
'text' => __('Message data'),
|
||||
'open' => true,
|
||||
],
|
||||
sprintf('<pre class="p-2 rounded mb-0" style="background: #eeeeee55;"><code>%s</code></pre>', json_encode($request['data']['sent'], JSON_PRETTY_PRINT))
|
||||
|
|
|
@ -60,9 +60,7 @@ class TagHelper extends Helper
|
|||
'icon' => 'plus',
|
||||
'variant' => 'secondary',
|
||||
'class' => ['badge'],
|
||||
'params' => [
|
||||
'onclick' => 'createTagPicker(this)',
|
||||
]
|
||||
'onclick' => 'createTagPicker(this)',
|
||||
]);
|
||||
} else {
|
||||
$html .= '<script>$(document).ready(function() { initSelect2Pickers() })</script>';
|
||||
|
@ -111,22 +109,20 @@ class TagHelper extends Helper
|
|||
'class' => ['ms-1', 'border-0', "text-${textColour}"],
|
||||
'variant' => 'text',
|
||||
'title' => __('Delete tag'),
|
||||
'params' => [
|
||||
'onclick' => sprintf('deleteTag(\'%s\', \'%s\', this)',
|
||||
$this->Url->build([
|
||||
'controller' => $this->getView()->getName(),
|
||||
'action' => 'untag',
|
||||
$this->getView()->get('entity')['id']
|
||||
]),
|
||||
h($tag['name'])
|
||||
),
|
||||
],
|
||||
'onclick' => sprintf('deleteTag(\'%s\', \'%s\', this)',
|
||||
$this->Url->build([
|
||||
'controller' => $this->getView()->getName(),
|
||||
'action' => 'untag',
|
||||
$this->getView()->get('entity')['id']
|
||||
]),
|
||||
h($tag['name'])
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
$deleteButton = '';
|
||||
}
|
||||
|
||||
$html = $this->Bootstrap->genNode('span', [
|
||||
$html = $this->Bootstrap->node('span', [
|
||||
'class' => [
|
||||
'tag',
|
||||
'badge',
|
||||
|
|
|
@ -120,8 +120,9 @@ class Application extends BaseApplication implements AuthenticationServiceProvid
|
|||
]));
|
||||
\SocialConnect\JWX\JWT::$screw = Configure::check('keycloak.screw') ? Configure::read('keycloak.screw') : 0;
|
||||
}
|
||||
$middlewareQueue->add(new AuthenticationMiddleware($this))
|
||||
->add(new BodyParserMiddleware());
|
||||
$middlewareQueue
|
||||
->add(new BodyParserMiddleware())
|
||||
->add(new AuthenticationMiddleware($this));
|
||||
return $middlewareQueue;
|
||||
}
|
||||
|
||||
|
|
|
@ -90,24 +90,39 @@ class BroodsController extends AppController
|
|||
|
||||
public function testConnection($id)
|
||||
{
|
||||
$this->request->getSession()->close(); // close session to allow concurrent requests
|
||||
$status = $this->Broods->queryStatus($id);
|
||||
return $this->RestResponse->viewData($status, 'json');
|
||||
}
|
||||
|
||||
public function previewIndex($id, $scope)
|
||||
{
|
||||
if (!in_array($scope, ['organisations', 'individuals', 'sharingGroups'])) {
|
||||
throw new MethodNotAllowedException(__('Invalid scope. Valid options are: organisations, individuals, sharing_groups'));
|
||||
$validScopes = array_keys($this->Broods->previewScopes);
|
||||
if (!in_array($scope, $validScopes)) {
|
||||
throw new MethodNotAllowedException(__('Invalid scope. Valid options are: {0}', implode(', ', $validScopes)));
|
||||
}
|
||||
$filter = $this->request->getQuery('quickFilter');
|
||||
$data = $this->Broods->queryIndex($id, $scope, $filter);
|
||||
$filtering = [
|
||||
'page' => $this->request->getQuery('page', 1),
|
||||
'limit' => $this->request->getQuery('limit', 20),
|
||||
];
|
||||
if (!empty($this->request->getQuery('quickFilter'))) {
|
||||
$filtering['quickFilter'] = $this->request->getQuery('quickFilter');
|
||||
}
|
||||
$data = $this->Broods->queryIndex($id, $scope, $filtering, true);
|
||||
if (!is_array($data)) {
|
||||
$data = [];
|
||||
}
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
return $this->RestResponse->viewData($data, 'json');
|
||||
} else {
|
||||
$data = $this->Broods->attachAllSyncStatus($data, $scope);
|
||||
$data = $this->CustomPagination->paginate($data);
|
||||
$optionFilters = ['quickFilter'];
|
||||
$CRUDParams = $this->ParamHandler->harvestParams($optionFilters);
|
||||
$CRUDOptions = [
|
||||
'quickFilters' => $this->Broods->previewScopes[$scope]['quickFilterFields'],
|
||||
];
|
||||
$this->CRUD->setQuickFilterForView($CRUDParams, $CRUDOptions);
|
||||
$this->set('data', $data);
|
||||
$this->set('brood_id', $id);
|
||||
if ($this->request->is('ajax')) {
|
||||
|
@ -120,23 +135,43 @@ class BroodsController extends AppController
|
|||
|
||||
public function downloadOrg($brood_id, $org_id)
|
||||
{
|
||||
$result = $this->Broods->downloadOrg($brood_id, $org_id);
|
||||
$success = __('Organisation fetched from remote.');
|
||||
$fail = __('Could not save the remote organisation');
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
if ($result) {
|
||||
return $this->RestResponse->saveSuccessResponse('Brood', 'downloadOrg', $brood_id, 'json', $success);
|
||||
if ($this->request->is('post')) {
|
||||
$result = $this->Broods->downloadOrg($brood_id, $org_id);
|
||||
$success = __('Organisation fetched from remote.');
|
||||
$fail = __('Could not save the remote organisation');
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
if ($result) {
|
||||
return $this->RestResponse->saveSuccessResponse('Brood', 'downloadOrg', $brood_id, 'json', $success);
|
||||
} else {
|
||||
return $this->RestResponse->saveFailResponse('Brood', 'downloadOrg', $brood_id, $fail, 'json');
|
||||
}
|
||||
} else {
|
||||
return $this->RestResponse->saveFailResponse('Brood', 'downloadOrg', $brood_id, $fail, 'json');
|
||||
if ($result) {
|
||||
$this->Flash->success($success);
|
||||
} else {
|
||||
$this->Flash->error($fail);
|
||||
}
|
||||
$this->redirect($this->referer());
|
||||
}
|
||||
} else {
|
||||
if ($result) {
|
||||
$this->Flash->success($success);
|
||||
} else {
|
||||
$this->Flash->error($fail);
|
||||
}
|
||||
$this->redirect($this->referer());
|
||||
}
|
||||
if ($org_id === 'all') {
|
||||
$question = __('All organisations from brood `{0}` will be downloaded. Continue?', h($brood_id));
|
||||
$title = __('Download all organisations from this brood');
|
||||
$actionName = __('Download all');
|
||||
} else {
|
||||
$question = __('The organisations `{0}` from brood `{1}` will be downloaded. Continue?', h($org_id), h($brood_id));
|
||||
$title = __('Download organisation from this brood');
|
||||
$actionName = __('Download organisation');
|
||||
}
|
||||
$this->set('title', $title);
|
||||
$this->set('question', $question);
|
||||
$this->set('modalOptions', [
|
||||
'confirmButton' => [
|
||||
'variant' => $org_id === 'all' ? 'warning' : 'primary',
|
||||
'text' => $actionName,
|
||||
],
|
||||
]);
|
||||
$this->render('/genericTemplates/confirm');
|
||||
}
|
||||
|
||||
public function downloadIndividual($brood_id, $individual_id)
|
||||
|
|
|
@ -16,17 +16,17 @@ use Cake\Collection\Collection;
|
|||
|
||||
class APIRearrangeComponent extends Component
|
||||
{
|
||||
public function rearrangeForAPI(object $data)
|
||||
public static function rearrangeForAPI(object $data, array $options = [])
|
||||
{
|
||||
if (is_subclass_of($data, 'Iterator')) {
|
||||
$newData = [];
|
||||
$data->each(function ($value, $key) use (&$newData) {
|
||||
$value->rearrangeForAPI();
|
||||
$data->each(function ($value, $key) use (&$newData, $options) {
|
||||
$value->rearrangeForAPI($options);
|
||||
$newData[] = $value;
|
||||
});
|
||||
return new Collection($newData);
|
||||
} else {
|
||||
$data->rearrangeForAPI();
|
||||
$data->rearrangeForAPI($options);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
|
|
@ -33,6 +33,10 @@ class CRUDComponent extends Component
|
|||
|
||||
public function index(array $options): void
|
||||
{
|
||||
$embedInModal = !empty($this->request->getQuery('embedInModal', false));
|
||||
$excludeStats = !empty($this->request->getQuery('excludeStats', false));
|
||||
$skipTableToolbar = !empty($this->request->getQuery('skipTableToolbar', false));
|
||||
|
||||
if (!empty($options['quickFilters'])) {
|
||||
if (empty($options['filters'])) {
|
||||
$options['filters'] = [];
|
||||
|
@ -46,7 +50,8 @@ class CRUDComponent extends Component
|
|||
$options['filters'][] = 'filteringTags';
|
||||
}
|
||||
|
||||
$optionFilters = empty($options['filters']) ? [] : $options['filters'];
|
||||
$optionFilters = [];
|
||||
$optionFilters += empty($options['filters']) ? [] : $options['filters'];
|
||||
foreach ($optionFilters as $i => $filter) {
|
||||
$optionFilters[] = "{$filter} !=";
|
||||
$optionFilters[] = "{$filter} >=";
|
||||
|
@ -79,11 +84,16 @@ class CRUDComponent extends Component
|
|||
$this->Controller->paginate['order'] = $options['order'];
|
||||
}
|
||||
}
|
||||
if ($this->metaFieldsSupported() && !$this->Controller->ParamHandler->isRest()) {
|
||||
$query = $this->includeRequestedMetaFields($query);
|
||||
}
|
||||
if (!$this->Controller->ParamHandler->isRest()) {
|
||||
$this->setRequestedEntryAmount();
|
||||
}
|
||||
$data = $this->Controller->paginate($query, $this->Controller->paginate ?? []);
|
||||
$totalCount = $this->Controller->getRequest()->getAttribute('paging')[$this->TableAlias]['count'];
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
if ($this->metaFieldsSupported()) {
|
||||
$query = $this->includeRequestedMetaFields($query);
|
||||
}
|
||||
$data = $query->all();
|
||||
$data = $this->Controller->paginate($query, $this->Controller->paginate ?? []);
|
||||
if (isset($options['hidden'])) {
|
||||
$data->each(function($value, $key) use ($options) {
|
||||
$hidden = is_array($options['hidden']) ? $options['hidden'] : [$options['hidden']];
|
||||
|
@ -114,13 +124,11 @@ class CRUDComponent extends Component
|
|||
return $this->attachMetaTemplatesIfNeeded($value, $metaTemplates);
|
||||
});
|
||||
}
|
||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
|
||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json', false, false, false, [
|
||||
'X-Total-Count' => $totalCount,
|
||||
]);
|
||||
} else {
|
||||
if ($this->metaFieldsSupported()) {
|
||||
$query = $this->includeRequestedMetaFields($query);
|
||||
}
|
||||
$this->setRequestedEntryAmount();
|
||||
$data = $this->Controller->paginate($query, $this->Controller->paginate ?? []);
|
||||
$this->Controller->setResponse($this->Controller->getResponse()->withHeader('X-Total-Count', $totalCount));
|
||||
if (isset($options['afterFind'])) {
|
||||
$function = $options['afterFind'];
|
||||
if (is_callable($function)) {
|
||||
|
@ -150,7 +158,7 @@ class CRUDComponent extends Component
|
|||
return $template['enabled'];
|
||||
}));
|
||||
}
|
||||
if (true) { // check if stats are requested
|
||||
if (empty($excludeStats)) { // check if stats are requested
|
||||
$modelStatistics = [];
|
||||
if ($this->Table->hasBehavior('Timestamp')) {
|
||||
$modelStatistics = $this->Table->getActivityStatisticsForModel(
|
||||
|
@ -191,6 +199,8 @@ class CRUDComponent extends Component
|
|||
}
|
||||
$this->Controller->set('model', $this->Table);
|
||||
$this->Controller->set('data', $data);
|
||||
$this->Controller->set('embedInModal', $embedInModal);
|
||||
$this->Controller->set('skipTableToolbar', $skipTableToolbar);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -439,6 +449,7 @@ class CRUDComponent extends Component
|
|||
'field' => $rawMetaTemplateField->field,
|
||||
'meta_template_id' => $rawMetaTemplateField->meta_template_id,
|
||||
'meta_template_field_id' => $rawMetaTemplateField->id,
|
||||
'meta_template_directory_id' => $allMetaTemplates[$template_id]->meta_template_directory_id,
|
||||
'parent_id' => $entity->id,
|
||||
'uuid' => Text::uuid(),
|
||||
]);
|
||||
|
@ -469,6 +480,7 @@ class CRUDComponent extends Component
|
|||
'field' => $rawMetaTemplateField->field,
|
||||
'meta_template_id' => $rawMetaTemplateField->meta_template_id,
|
||||
'meta_template_field_id' => $rawMetaTemplateField->id,
|
||||
'meta_template_directory_id' => $template->meta_template_directory_id,
|
||||
'parent_id' => $entity->id,
|
||||
'uuid' => Text::uuid(),
|
||||
]);
|
||||
|
@ -1126,17 +1138,9 @@ class CRUDComponent extends Component
|
|||
|
||||
public function setQuickFilters(array $params, \Cake\ORM\Query $query, array $options): \Cake\ORM\Query
|
||||
{
|
||||
$this->setQuickFilterForView($params, $options);
|
||||
$quickFilterFields = $options['quickFilters'];
|
||||
$queryConditions = [];
|
||||
$this->Controller->set('quickFilter', empty($quickFilterFields) ? [] : $quickFilterFields);
|
||||
if ($this->metaFieldsSupported() && !empty($options['quickFilterForMetaField']['enabled'])) {
|
||||
$this->Controller->set('quickFilterForMetaField', [
|
||||
'enabled' => $options['quickFilterForMetaField']['enabled'] ?? false,
|
||||
'wildcard_search' => $options['quickFilterForMetaField']['enabled'] ?? false,
|
||||
]);
|
||||
}
|
||||
if (!empty($params['quickFilter']) && !empty($quickFilterFields)) {
|
||||
$this->Controller->set('quickFilterValue', $params['quickFilter']);
|
||||
$queryConditions = $this->genQuickFilterConditions($params, $quickFilterFields);
|
||||
|
||||
if ($this->metaFieldsSupported() && !empty($options['quickFilterForMetaField']['enabled'])) {
|
||||
|
@ -1146,10 +1150,25 @@ class CRUDComponent extends Component
|
|||
}
|
||||
|
||||
$query->where(['OR' => $queryConditions]);
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function setQuickFilterForView(array $params, array $options): void
|
||||
{
|
||||
$quickFilterFields = $options['quickFilters'];
|
||||
$this->Controller->set('quickFilter', empty($quickFilterFields) ? [] : $quickFilterFields);
|
||||
if ($this->metaFieldsSupported() && !empty($options['quickFilterForMetaField']['enabled'])) {
|
||||
$this->Controller->set('quickFilterForMetaField', [
|
||||
'enabled' => $options['quickFilterForMetaField']['enabled'] ?? false,
|
||||
'wildcard_search' => $options['quickFilterForMetaField']['enabled'] ?? false,
|
||||
]);
|
||||
}
|
||||
if (!empty($params['quickFilter']) && !empty($quickFilterFields)) {
|
||||
$this->Controller->set('quickFilterValue', $params['quickFilter']);
|
||||
} else {
|
||||
$this->Controller->set('quickFilterValue', '');
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function genQuickFilterConditions(array $params, array $quickFilterFields): array
|
||||
|
@ -1561,8 +1580,11 @@ class CRUDComponent extends Component
|
|||
private function renderViewInVariable($templateRelativeName, $data)
|
||||
{
|
||||
$builder = new ViewBuilder();
|
||||
$builder->disableAutoLayout()->setTemplate("{$this->TableAlias}/{$templateRelativeName}");
|
||||
$view = $builder->build($data);
|
||||
$builder->disableAutoLayout()
|
||||
->setClassName('Monad')
|
||||
->setTemplate("{$this->TableAlias}/{$templateRelativeName}")
|
||||
->setVars($data);
|
||||
$view = $builder->build();
|
||||
return $view->render();
|
||||
}
|
||||
|
||||
|
|
|
@ -14,12 +14,14 @@ class InboxNavigation extends BaseNavigation
|
|||
'icon' => 'trash',
|
||||
'url' => '/inbox/discard/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
'isPOST' => true,
|
||||
]);
|
||||
$this->bcf->addRoute('Inbox', 'process', [
|
||||
'label' => __('Process message'),
|
||||
'icon' => 'cogs',
|
||||
'url' => '/inbox/process/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
'isPOST' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,16 +11,18 @@ class MetaTemplatesNavigation extends BaseNavigation
|
|||
$this->bcf->addRoute('MetaTemplates', 'index', $this->bcf->defaultCRUD('MetaTemplates', 'index'));
|
||||
$this->bcf->addRoute('MetaTemplates', 'view', $this->bcf->defaultCRUD('MetaTemplates', 'view'));
|
||||
$this->bcf->addRoute('MetaTemplates', 'enable', [
|
||||
'label' => __('Enable'),
|
||||
'label' => __('Enable / Disable'),
|
||||
'icon' => 'check-square',
|
||||
'url' => '/metaTemplates/enable/{{id}}/enabled',
|
||||
'url' => '/metaTemplates/toggle/{{id}}/enabled',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
'isPOST' => true,
|
||||
]);
|
||||
$this->bcf->addRoute('MetaTemplates', 'set_default', [
|
||||
'label' => __('Set as default'),
|
||||
'icon' => 'check-square',
|
||||
'url' => '/metaTemplates/toggle/{{id}}/default',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
'isPOST' => true,
|
||||
]);
|
||||
|
||||
$totalUpdateCount = 0;
|
||||
|
@ -46,11 +48,13 @@ class MetaTemplatesNavigation extends BaseNavigation
|
|||
'label' => __('Update template'),
|
||||
'icon' => 'download',
|
||||
'url' => '/metaTemplates/update',
|
||||
'isPOST' => true,
|
||||
]);
|
||||
$this->bcf->addRoute('MetaTemplates', 'prune_outdated_template', [
|
||||
'label' => __('Prune outdated template'),
|
||||
'icon' => 'trash',
|
||||
'url' => '/metaTemplates/prune_outdated_template',
|
||||
'isPOST' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -74,7 +78,7 @@ class MetaTemplatesNavigation extends BaseNavigation
|
|||
$totalUpdateCount = $udpateCount + $newCount;
|
||||
}
|
||||
$updateAllActionConfig = [
|
||||
'label' => __('Update template'),
|
||||
'label' => __('Update all template'),
|
||||
'url' => '/metaTemplates/updateAllTemplates',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
];
|
||||
|
@ -87,11 +91,11 @@ class MetaTemplatesNavigation extends BaseNavigation
|
|||
}
|
||||
$this->bcf->addAction('MetaTemplates', 'index', 'MetaTemplates', 'update_all_templates', $updateAllActionConfig);
|
||||
$this->bcf->addAction('MetaTemplates', 'index', 'MetaTemplates', 'prune_outdated_template', [
|
||||
'label' => __('Prune outdated template'),
|
||||
'label' => __('Prune outdated templates'),
|
||||
'url' => '/metaTemplates/prune_outdated_template',
|
||||
]);
|
||||
|
||||
if (empty($this->viewVars['updateableTemplates']['up-to-date'])) {
|
||||
if (empty($this->viewVars['templateStatus']['up-to-date'])) {
|
||||
$this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'update', [
|
||||
'label' => __('Update template'),
|
||||
'url' => '/metaTemplates/update/{{id}}',
|
||||
|
|
|
@ -14,12 +14,14 @@ class OutboxNavigation extends BaseNavigation
|
|||
'icon' => 'trash',
|
||||
'url' => '/outbox/discard/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
'isPOST' => true,
|
||||
]);
|
||||
$this->bcf->addRoute('Outbox', 'process', [
|
||||
'label' => __('Process message'),
|
||||
'icon' => 'cogs',
|
||||
'url' => '/outbox/process/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
'isPOST' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -217,6 +217,7 @@ class BreadcrumbFactory
|
|||
'label' => __('[new {0}]', $controller),
|
||||
'icon' => 'plus',
|
||||
'url' => "/{$controller}/add",
|
||||
'isPOST' => true,
|
||||
]);
|
||||
} else if ($action === 'edit') {
|
||||
$item = $this->genRouteConfig($controller, $action, [
|
||||
|
@ -224,6 +225,7 @@ class BreadcrumbFactory
|
|||
'icon' => 'edit',
|
||||
'url' => "/{$controller}/edit/{{id}}",
|
||||
'url_vars' => ['id' => 'id'],
|
||||
'isPOST' => true,
|
||||
'textGetter' => !empty($table->getDisplayField()) ? $table->getDisplayField() : 'id',
|
||||
]);
|
||||
} else if ($action === 'delete') {
|
||||
|
@ -232,6 +234,15 @@ class BreadcrumbFactory
|
|||
'icon' => 'trash',
|
||||
'url' => "/{$controller}/delete/{{id}}",
|
||||
'url_vars' => ['id' => 'id'],
|
||||
'isPOST' => true,
|
||||
'textGetter' => !empty($table->getDisplayField()) ? $table->getDisplayField() : 'id',
|
||||
]);
|
||||
} else if ($action === 'audit') {
|
||||
$item = $this->genRouteConfig($controller, $action, [
|
||||
'label' => __('Audit changes'),
|
||||
'icon' => 'history',
|
||||
'url' => "/audit-logs?model={{model}}&model_id={{id}}&sort=created&direction=desc&embedInModal=1&excludeStats=1&skipTableToolbar=1",
|
||||
'url_vars' => ['id' => 'id', 'model' => ['raw' => $table->getAlias()]],
|
||||
'textGetter' => !empty($table->getDisplayField()) ? $table->getDisplayField() : 'id',
|
||||
]);
|
||||
}
|
||||
|
@ -253,6 +264,7 @@ class BreadcrumbFactory
|
|||
$routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'label');
|
||||
$routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'textGetter');
|
||||
$routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'badge');
|
||||
$routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'isPOST');
|
||||
return $routeConfig;
|
||||
}
|
||||
|
||||
|
@ -279,6 +291,7 @@ class BreadcrumbFactory
|
|||
$this->addRoute($controller, 'add', $this->defaultCRUD($controller, 'add'));
|
||||
$this->addRoute($controller, 'edit', $this->defaultCRUD($controller, 'edit'));
|
||||
$this->addRoute($controller, 'delete', $this->defaultCRUD($controller, 'delete'));
|
||||
$this->addRoute($controller, 'audit', $this->defaultCRUD($controller, 'audit'));
|
||||
|
||||
$this->addParent($controller, 'view', $controller, 'index');
|
||||
$this->addParent($controller, 'add', $controller, 'index');
|
||||
|
@ -292,8 +305,10 @@ class BreadcrumbFactory
|
|||
|
||||
$this->addAction($controller, 'view', $controller, 'add');
|
||||
$this->addAction($controller, 'view', $controller, 'delete');
|
||||
$this->addAction($controller, 'view', $controller, 'audit');
|
||||
$this->addAction($controller, 'edit', $controller, 'add');
|
||||
$this->addAction($controller, 'edit', $controller, 'delete');
|
||||
$this->addAction($controller, 'edit', $controller, 'audit');
|
||||
}
|
||||
|
||||
public function get($controller, $action)
|
||||
|
|
|
@ -283,6 +283,7 @@ class RestResponseComponent extends Component
|
|||
private $__scopedFieldsConstraint = array();
|
||||
|
||||
public function initialize(array $config): void {
|
||||
parent::initialize($config);
|
||||
$this->__configureFieldConstraints();
|
||||
$this->Controller = $this->getController();
|
||||
}
|
||||
|
@ -559,7 +560,14 @@ class RestResponseComponent extends Component
|
|||
$data['errors'] = $errors;
|
||||
}
|
||||
if (!$raw && is_object($data)) {
|
||||
$data = $this->APIRearrange->rearrangeForAPI($data);
|
||||
$rearrangeOptions = [];
|
||||
if (!empty($this->Controller->getRequest()->getQuery('includeMetatemplate', false))) {
|
||||
$rearrangeOptions['includeMetatemplate'] = true;
|
||||
}
|
||||
if (!empty($this->Controller->getRequest()->getQuery('includeFullMetaFields', false))) {
|
||||
$rearrangeOptions['includeFullMetaFields'] = true;
|
||||
}
|
||||
$data = $this->APIRearrange->rearrangeForAPI($data, $rearrangeOptions);
|
||||
}
|
||||
return $this->__sendResponse($data, 200, $format, $raw, $download, $headers);
|
||||
}
|
||||
|
|
|
@ -226,13 +226,16 @@ class MetaTemplatesController extends AppController
|
|||
foreach ($newestMetaTemplate->meta_template_fields as $i => $newMetaTemplateField) {
|
||||
if ($metaTemplateField->field == $newMetaTemplateField->field && empty($newMetaTemplateField->metaFields)) {
|
||||
$movedMetaTemplateFields[] = $metaTemplateField->id;
|
||||
$nonEmptyMetaFields = array_filter($metaTemplateField->metaFields, function ($e) {
|
||||
return $e->value !== '';
|
||||
});
|
||||
$copiedMetaFields = array_map(function ($e) use ($newMetaTemplateField) {
|
||||
$e = $e->toArray();
|
||||
$e['meta_template_id'] = $newMetaTemplateField->meta_template_id;
|
||||
$e['meta_template_field_id'] = $newMetaTemplateField->id;
|
||||
unset($e['id']);
|
||||
return $e;
|
||||
}, $metaTemplateField->metaFields);
|
||||
}, $nonEmptyMetaFields);
|
||||
$newMetaTemplateField->metaFields = $this->MetaTemplates->MetaTemplateFields->MetaFields->newEntities($copiedMetaFields);
|
||||
}
|
||||
}
|
||||
|
@ -244,6 +247,55 @@ class MetaTemplatesController extends AppController
|
|||
$this->set('movedMetaTemplateFields', $movedMetaTemplateFields);
|
||||
}
|
||||
|
||||
public function migrateMetafieldsToNewestTemplate(int $template_id, $forceMigration=false)
|
||||
{
|
||||
$oldMetaTemplate = $this->MetaTemplates->find()->where([
|
||||
'id' => $template_id
|
||||
])->contain(['MetaTemplateFields'])->first();
|
||||
|
||||
if (empty($oldMetaTemplate)) {
|
||||
throw new NotFoundException(__('Invalid {0} {1}.', $this->MetaTemplates->getAlias(), $template_id));
|
||||
}
|
||||
$newestMetaTemplate = $this->MetaTemplates->getNewestVersion($oldMetaTemplate, true);
|
||||
if ($oldMetaTemplate->id == $newestMetaTemplate->id) {
|
||||
throw new NotFoundException(__('Invalid {0} {1}. Template already the newest version', $this->MetaTemplates->getAlias(), $template_id));
|
||||
}
|
||||
|
||||
if ($this->request->is('post')) {
|
||||
$result = $this->MetaTemplates->migrateMetafieldsToNewestTemplate($oldMetaTemplate, $newestMetaTemplate, !empty($forceMigration));
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
return $this->RestResponse->viewData($result, 'json');
|
||||
} else {
|
||||
if ($result['success']) {
|
||||
$message = __('{0} entities updated. {1} entities could not be automatically migrated.', $result['migrated_count'], $result['conflicting_entities']);
|
||||
} else {
|
||||
$message = __('{0} entities updated. {1} entities could not be automatically migrated. {2} entities could not be updated due to errors', $result['migrated_count'], $result['conflicting_entities'], $result['migration_errors']);
|
||||
}
|
||||
$this->CRUD->setResponseForController('update', $result['success'], $message, $result, $result['migration_errors'], ['redirect' => $this->referer()]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$entities = $this->MetaTemplates->getEntitiesHavingMetaFieldsFromTemplate($oldMetaTemplate->id, null);
|
||||
$conflictingEntities = [];
|
||||
foreach ($entities as $entity) {
|
||||
$conflicts = $this->MetaTemplates->getMetaFieldsConflictsUnderTemplate($entity->meta_fields, $newestMetaTemplate);
|
||||
if (!empty($conflicts)) {
|
||||
$conflictingEntities[] = $entity;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->ParamHandler->isRest()) {
|
||||
$this->set('oldMetaTemplate', $oldMetaTemplate);
|
||||
$this->set('newestMetaTemplate', $newestMetaTemplate);
|
||||
$this->set('conflictingEntities', $conflictingEntities);
|
||||
$this->set('entityCount', count($entities));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$templatesUpdateStatus = $this->MetaTemplates->getUpdateStatusForTemplates();
|
||||
|
|
|
@ -65,6 +65,11 @@ class OrganisationsController extends AppController
|
|||
]
|
||||
];
|
||||
}
|
||||
$additionalContainFields = [];
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
$additionalContainFields[] = 'MetaFields';
|
||||
}
|
||||
$containFields = array_merge($this->containFields, $additionalContainFields);
|
||||
|
||||
$this->CRUD->index([
|
||||
'filters' => $this->filterFields,
|
||||
|
@ -73,7 +78,7 @@ class OrganisationsController extends AppController
|
|||
'contextFilters' => [
|
||||
'custom' => $customContextFilters,
|
||||
],
|
||||
'contain' => $this->containFields,
|
||||
'contain' => $containFields,
|
||||
'statisticsFields' => $this->statisticsFields,
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
|
|
|
@ -160,7 +160,7 @@ class SharingGroupsController extends AppController
|
|||
'organisation' => $this->SharingGroups->Organisations->find('list', [
|
||||
'sort' => ['name' => 'asc'],
|
||||
'conditions' => $conditions
|
||||
])
|
||||
])->toArray()
|
||||
];
|
||||
if ($this->request->is('post')) {
|
||||
$input = $this->request->getData();
|
||||
|
@ -280,7 +280,7 @@ class SharingGroupsController extends AppController
|
|||
'conditions' => [
|
||||
'id' => $user['organisation_id']
|
||||
]
|
||||
]);
|
||||
])->toArray();
|
||||
}
|
||||
return $organisations;
|
||||
}
|
||||
|
|
|
@ -26,8 +26,13 @@ class UsersController extends AppController
|
|||
if (!empty(Configure::read('keycloak.enabled'))) {
|
||||
// $keycloakUsersParsed = $this->Users->getParsedKeycloakUser();
|
||||
}
|
||||
$additionalContainFields = [];
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
$additionalContainFields[] = 'MetaFields';
|
||||
}
|
||||
$containFields = array_merge($this->containFields, $additionalContainFields);
|
||||
$this->CRUD->index([
|
||||
'contain' => $this->containFields,
|
||||
'contain' => $containFields,
|
||||
'filters' => $this->filterFields,
|
||||
'quickFilters' => $this->quickFilterFields,
|
||||
'conditions' => $conditions,
|
||||
|
@ -154,7 +159,12 @@ class UsersController extends AppController
|
|||
}
|
||||
$keycloakUsersParsed = null;
|
||||
if (!empty(Configure::read('keycloak.enabled'))) {
|
||||
$keycloakUsersParsed = $this->Users->getParsedKeycloakUser();
|
||||
try {
|
||||
$keycloakUsersParsed = $this->Users->getParsedKeycloakUser();
|
||||
} catch (\Exception $e) {
|
||||
$keycloakUsersParsed = [];
|
||||
$this->Flash->error(__('Issue while connecting to keycloak. {0}', $e->getMessage()));
|
||||
}
|
||||
}
|
||||
$this->CRUD->view($id, [
|
||||
'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations'],
|
||||
|
@ -298,6 +308,9 @@ class UsersController extends AppController
|
|||
}
|
||||
$params = [
|
||||
'beforeSave' => function($data) use ($currentUser, $validRoles) {
|
||||
if (empty(Configure::read('user.allow-user-deletion'))) {
|
||||
throw new MethodNotAllowedException(__('User deletion is disabled on this instance.'));
|
||||
}
|
||||
if (!$currentUser['role']['perm_admin']) {
|
||||
if ($data['organisation_id'] !== $currentUser['organisation_id']) {
|
||||
throw new MethodNotAllowedException(__('You do not have permission to delete the given user.'));
|
||||
|
|
|
@ -459,7 +459,7 @@ class AuthKeycloakBehavior extends Behavior
|
|||
$requireUpdate = true;
|
||||
$differences = [
|
||||
'user' => [
|
||||
'keycloak' => 'USER NOT FOUND',
|
||||
'keycloak' => __('ERROR or USER NOT FOUND'),
|
||||
'cerebrate' => $user['username']
|
||||
]
|
||||
];
|
||||
|
|
|
@ -40,28 +40,47 @@ class AppModel extends Entity
|
|||
return TableRegistry::get($this->getSource());
|
||||
}
|
||||
|
||||
public function rearrangeForAPI(): void
|
||||
public function rearrangeForAPI(array $options = []): void
|
||||
{
|
||||
}
|
||||
|
||||
public function rearrangeMetaFields(): void
|
||||
public function rearrangeMetaFields(array $options = []): void
|
||||
{
|
||||
$this->meta_fields = [];
|
||||
foreach ($this->MetaTemplates as $template) {
|
||||
foreach ($template['meta_template_fields'] as $field) {
|
||||
if ($field['counter'] > 0) {
|
||||
foreach ($field['metaFields'] as $metaField) {
|
||||
if (!empty($this->meta_fields[$template['name']][$field['field']])) {
|
||||
if (!is_array($this->meta_fields[$template['name']][$field['field']])) {
|
||||
$this->meta_fields[$template['name']][$field['field']] = [$this->meta_fields[$template['name']][$field['field']]];
|
||||
if (!empty($options['includeFullMetaFields'])) {
|
||||
$this->meta_fields = [];
|
||||
foreach ($this->MetaTemplates as $template) {
|
||||
foreach ($template['meta_template_fields'] as $field) {
|
||||
if ($field['counter'] > 0) {
|
||||
foreach ($field['metaFields'] as $metaField) {
|
||||
if (!empty($this->meta_fields[$template['name']][$field['field']])) {
|
||||
if (!is_array($this->meta_fields[$template['name']][$field['field']])) {
|
||||
$this->meta_fields[$template['name']][$field['field']] = [$this->meta_fields[$template['name']][$field['field']]];
|
||||
}
|
||||
$this->meta_fields[$template['name']][$field['field']][] = $metaField['value'];
|
||||
} else {
|
||||
$this->meta_fields[$template['name']][$field['field']] = $metaField['value'];
|
||||
}
|
||||
$this->meta_fields[$template['name']][$field['field']][] = $metaField['value'];
|
||||
} else {
|
||||
$this->meta_fields[$template['name']][$field['field']] = $metaField['value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif (!empty($this->meta_fields)) {
|
||||
$templateDirectoryTable = TableRegistry::get('MetaTemplateNameDirectory');
|
||||
$templates = [];
|
||||
foreach ($this->meta_fields as $i => $metafield) {
|
||||
$templateDirectoryId = $metafield['meta_template_directory_id'];
|
||||
if (empty($templates[$templateDirectoryId])) {
|
||||
$templates[$templateDirectoryId] = $templateDirectoryTable->find()->where(['id' => $templateDirectoryId])->first();
|
||||
}
|
||||
$this->meta_fields[$i]['template_uuid'] = $templates[$templateDirectoryId]['uuid'];
|
||||
$this->meta_fields[$i]['template_version'] = $templates[$templateDirectoryId]['version'];
|
||||
$this->meta_fields[$i]['template_name'] = $templates[$templateDirectoryId]['name'];
|
||||
$this->meta_fields[$i]['template_namespace'] = $templates[$templateDirectoryId]['namespace'];
|
||||
}
|
||||
}
|
||||
// if ((!isset($options['includeMetatemplate']) || empty($options['includeMetatemplate'])) && !empty($this->MetaTemplates)) {
|
||||
if ((!isset($options['includeMetatemplate']) || empty($options['includeMetatemplate']))) {
|
||||
unset($this->MetaTemplates);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ class AuditLog extends AppModel
|
|||
return $title;
|
||||
}
|
||||
|
||||
public function rearrangeForAPI(): void
|
||||
public function rearrangeForAPI(array $options = []): void
|
||||
{
|
||||
if (!empty($this->user)) {
|
||||
$this->user = $this->user->toArray();
|
||||
|
|
|
@ -8,7 +8,7 @@ use Cake\ORM\Entity;
|
|||
class EncryptionKey extends AppModel
|
||||
{
|
||||
|
||||
public function rearrangeForAPI(): void
|
||||
public function rearrangeForAPI(array $options = []): void
|
||||
{
|
||||
$this->rearrangeSimplify(['organisation', 'individual']);
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ class Individual extends AppModel
|
|||
return $emails;
|
||||
}
|
||||
|
||||
public function rearrangeForAPI(): void
|
||||
public function rearrangeForAPI(array $options = []): void
|
||||
{
|
||||
if (!empty($this->tags)) {
|
||||
$this->tags = $this->rearrangeTags($this->tags);
|
||||
|
@ -51,10 +51,7 @@ class Individual extends AppModel
|
|||
$this->alignments = $this->rearrangeAlignments($this->alignments);
|
||||
}
|
||||
if (!empty($this->meta_fields)) {
|
||||
$this->rearrangeMetaFields();
|
||||
}
|
||||
if (!empty($this->MetaTemplates)) {
|
||||
unset($this->MetaTemplates);
|
||||
$this->rearrangeMetaFields($options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Entity;
|
||||
|
||||
use App\Model\Entity\AppModel;
|
||||
use Cake\ORM\Entity;
|
||||
|
||||
class MetaTemplateNameDirectory extends AppModel
|
||||
{
|
||||
|
||||
}
|
|
@ -17,7 +17,7 @@ class Organisation extends AppModel
|
|||
'created' => true
|
||||
];
|
||||
|
||||
public function rearrangeForAPI(): void
|
||||
public function rearrangeForAPI(array $options = []): void
|
||||
{
|
||||
if (!empty($this->tags)) {
|
||||
$this->tags = $this->rearrangeTags($this->tags);
|
||||
|
@ -25,11 +25,9 @@ class Organisation extends AppModel
|
|||
if (!empty($this->alignments)) {
|
||||
$this->alignments = $this->rearrangeAlignments($this->alignments);
|
||||
}
|
||||
if (!empty($this->meta_fields)) {
|
||||
$this->rearrangeMetaFields();
|
||||
}
|
||||
if (!empty($this->MetaTemplates)) {
|
||||
unset($this->MetaTemplates);
|
||||
if (!empty($this->meta_fields) || !empty($this->MetaTemplates)) {
|
||||
$this->rearrangeMetaFields($options);
|
||||
}
|
||||
}
|
||||
// MetaTemplate object property is not unset!!
|
||||
}
|
||||
|
|
|
@ -49,16 +49,13 @@ class User extends AppModel
|
|||
}
|
||||
}
|
||||
|
||||
public function rearrangeForAPI(): void
|
||||
public function rearrangeForAPI(array $options = []): void
|
||||
{
|
||||
if (!empty($this->tags)) {
|
||||
$this->tags = $this->rearrangeTags($this->tags);
|
||||
}
|
||||
if (!empty($this->meta_fields)) {
|
||||
$this->rearrangeMetaFields();
|
||||
}
|
||||
if (!empty($this->MetaTemplates)) {
|
||||
unset($this->MetaTemplates);
|
||||
if (!empty($this->meta_fields) || !empty($this->MetaTemplates)) {
|
||||
$this->rearrangeMetaFields($options);
|
||||
}
|
||||
if (!empty($this->user_settings_by_name)) {
|
||||
$this->rearrangeUserSettings();
|
||||
|
|
|
@ -2,10 +2,15 @@
|
|||
|
||||
namespace App\Model\Table;
|
||||
|
||||
require_once APP . DS . 'Utility/Utils.php';
|
||||
use App\Model\Table\AppTable;
|
||||
use function App\Utility\Utils\array_diff_recursive;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\Core\Configure;
|
||||
use Cake\Utility\Inflector;
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\I18n\FrozenTime;
|
||||
use Cake\Http\Client;
|
||||
use Cake\Http\Client\Response;
|
||||
use Cake\Http\Exception\NotFoundException;
|
||||
|
@ -15,6 +20,27 @@ use Cake\Error\Debugger;
|
|||
|
||||
class BroodsTable extends AppTable
|
||||
{
|
||||
|
||||
public $previewScopes = [
|
||||
'organisations' => [
|
||||
'quickFilterFields' => ['uuid', ['name' => true],],
|
||||
'contain' => ['MetaFields' => ['MetaTemplateNameDirectory'], 'Tags'],
|
||||
'compareFields' => ['name', 'url', 'nationality', 'sector', 'type', 'contacts', 'modified', 'tags', 'meta_fields',],
|
||||
],
|
||||
'individuals' => [
|
||||
'quickFilterFields' => ['uuid', ['email' => true], ['first_name' => true], ['last_name' => true],],
|
||||
'contain' => ['MetaFields'],
|
||||
'compareFields' => ['email', 'first_name', 'last_name', 'position', 'modified', 'meta_fields', 'tags',],
|
||||
],
|
||||
'sharingGroups' => [
|
||||
'quickFilterFields' => ['uuid', ['name' => true],],
|
||||
'contain' => ['SharingGroupOrgs', 'Organisations'],
|
||||
'compareFields' => ['name', 'releasability', 'description', 'organisation_id', 'user_id', 'active', 'local', 'modified', 'organisation', 'sharing_group_orgs',],
|
||||
],
|
||||
];
|
||||
|
||||
private $metaFieldCompareFields = ['modified', 'value'];
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
|
@ -119,14 +145,16 @@ class BroodsTable extends AppTable
|
|||
return $result;
|
||||
}
|
||||
|
||||
public function queryIndex($id, $scope, $filter)
|
||||
public function queryIndex($id, $scope, $filter, $full = false)
|
||||
{
|
||||
$brood = $this->find()->where(['id' => $id])->first();
|
||||
if (empty($brood)) {
|
||||
throw new NotFoundException(__('Brood not found'));
|
||||
}
|
||||
$filterQuery = empty($filter) ? '' : '?quickFilter=' . urlencode($filter);
|
||||
$response = $this->HTTPClientGET(sprintf('/%s/index.json%s', $scope, $filterQuery), $brood);
|
||||
if (!empty($full)) {
|
||||
$filter['full'] = 1;
|
||||
}
|
||||
$response = $this->HTTPClientGET(sprintf('/%s/index.json?%s', $scope, http_build_query($filter)), $brood);
|
||||
if ($response->isOk()) {
|
||||
return $response->getJson();
|
||||
} else {
|
||||
|
@ -371,4 +399,124 @@ class BroodsTable extends AppTable
|
|||
$connector = $params['connector'][$params['remote_tool']['connector']];
|
||||
$connector->remoteToolConnectionStatus($params, constant(get_class($connector) . '::' . $status));
|
||||
}
|
||||
|
||||
public function attachAllSyncStatus(array $data, string $scope): array
|
||||
{
|
||||
$options = $this->previewScopes[$scope];
|
||||
foreach ($data as $i => $entry) {
|
||||
$data[$i] = $this->__attachSyncStatus($scope, $entry, $options);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function __attachSyncStatus(string $scope, array $entry, array $options = []): array
|
||||
{
|
||||
$table = TableRegistry::getTableLocator()->get(Inflector::camelize($scope));
|
||||
$localEntry = $table
|
||||
->find()
|
||||
->where(['uuid' => $entry['uuid']])
|
||||
->first();
|
||||
if (is_null($localEntry)) {
|
||||
$entry['status'] = $this->__statusNotLocal();
|
||||
} else {
|
||||
if (!empty($options['contain'])) {
|
||||
$localEntry = $table->loadInto($localEntry, $options['contain']);
|
||||
}
|
||||
$localEntry = json_decode(json_encode($localEntry), true);
|
||||
$entry['status'] = $this->__statusLocal($entry, $localEntry, $options);
|
||||
}
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
private function __statusNotLocal(): array
|
||||
{
|
||||
return self::__getStatus(false);
|
||||
}
|
||||
|
||||
private function __statusLocal(array $remoteEntry, $localEntry, array $options = []): array
|
||||
{
|
||||
$isLocalNewer = (new FrozenTime($localEntry['modified']))->toUnixString() >= (new FrozenTime($remoteEntry['modified']))->toUnixString();
|
||||
$compareFields = $options['compareFields'];
|
||||
$fieldDifference = [];
|
||||
$fieldDifference = array_diff_recursive($remoteEntry, $localEntry);
|
||||
// if (in_array('meta_fields', $options['compareFields']) && !empty($fieldDifference['meta_fields'])) {
|
||||
// $fieldDifference['meta_fields'] = $this->_compareMetaFields($remoteEntry, $localEntry, $options);
|
||||
// }
|
||||
$fieldDifference = array_filter($fieldDifference, function($value, $field) use ($compareFields) {
|
||||
return in_array($field, $compareFields);
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
foreach ($fieldDifference as $fieldName => $value) {
|
||||
$fieldDifference[$fieldName] = [
|
||||
'local' => $localEntry[$fieldName],
|
||||
'remote' => $value,
|
||||
];
|
||||
}
|
||||
if (in_array('meta_fields', $options['compareFields']) && !empty($fieldDifference['meta_fields'])) {
|
||||
$fieldDifference['meta_fields'] = $this->_compareMetaFields($remoteEntry, $localEntry, $options);
|
||||
}
|
||||
|
||||
return self::__getStatus(true, $isLocalNewer, $fieldDifference);
|
||||
}
|
||||
|
||||
private static function __getStatus($local=true, $updateToDate=false, array $data = []): array
|
||||
{
|
||||
$status = [
|
||||
'local' => $local,
|
||||
'up_to_date' => $updateToDate,
|
||||
'data' => $data,
|
||||
];
|
||||
if ($status['local'] && $status['up_to_date']) {
|
||||
$status['title'] = __('This entity is up-to-date');
|
||||
} else if ($status['local'] && !$status['up_to_date']) {
|
||||
$status['title'] = __('This entity is known but differs with the remote');
|
||||
} else {
|
||||
$status['title'] = __('This entity is not known locally');
|
||||
}
|
||||
return $status;
|
||||
}
|
||||
|
||||
private function _compareMetaFields($remoteEntry, $localEntry): array
|
||||
{
|
||||
$compareFields = $this->metaFieldCompareFields;
|
||||
$indexedRemoteMF = [];
|
||||
$indexedLocalMF = [];
|
||||
foreach ($remoteEntry['meta_fields'] as $metafields) {
|
||||
$indexedRemoteMF[$metafields['uuid']] = array_intersect_key($metafields, array_flip($compareFields));
|
||||
}
|
||||
foreach ($localEntry['meta_fields'] as $metafields) {
|
||||
$indexedLocalMF[$metafields['uuid']] = array_intersect_key($metafields, array_flip($compareFields));
|
||||
}
|
||||
$fieldDifference = [];
|
||||
foreach ($remoteEntry['meta_fields'] as $remoteMetafield) {
|
||||
$uuid = $remoteMetafield['uuid'];
|
||||
$metafieldName = $remoteMetafield['field'];
|
||||
// $metafieldName = sprintf('%s(v%s) :: %s', $remoteMetafield['template_name'], $remoteMetafield['template_version'], $remoteMetafield['field']);
|
||||
if (empty($fieldDifference[$metafieldName])) {
|
||||
$fieldDifference[$metafieldName] = [
|
||||
'meta_template' => [
|
||||
'name' => $remoteMetafield['template_name'],
|
||||
'version' => $remoteMetafield['template_version'],
|
||||
'uuid' => $remoteMetafield['template_uuid']
|
||||
],
|
||||
'delta' => [],
|
||||
];
|
||||
}
|
||||
if (empty($indexedLocalMF[$uuid])) {
|
||||
$fieldDifference[$metafieldName]['delta'][] = [
|
||||
'local' => null,
|
||||
'remote' => $indexedRemoteMF[$uuid],
|
||||
];
|
||||
} else {
|
||||
$fieldDifferenceTmp = array_diff_recursive($indexedRemoteMF[$uuid], $indexedLocalMF[$uuid]);
|
||||
if (!empty($fieldDifferenceTmp)) {
|
||||
$fieldDifference[$metafieldName]['delta'][] = [
|
||||
'local' => $indexedLocalMF[$uuid],
|
||||
'remote' => $indexedRemoteMF[$uuid],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $fieldDifference;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,9 @@ namespace App\Model\Table;
|
|||
use App\Model\Table\AppTable;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\Event\EventInterface;
|
||||
use Cake\ORM\RulesChecker;
|
||||
use ArrayObject;
|
||||
|
||||
class MetaFieldsTable extends AppTable
|
||||
{
|
||||
|
@ -22,6 +24,8 @@ class MetaFieldsTable extends AppTable
|
|||
$this->addBehavior('Timestamp');
|
||||
$this->belongsTo('MetaTemplates');
|
||||
$this->belongsTo('MetaTemplateFields');
|
||||
$this->belongsTo('MetaTemplateNameDirectory')
|
||||
->setForeignKey('meta_template_directory_id');
|
||||
|
||||
$this->setDisplayField('field');
|
||||
}
|
||||
|
@ -35,7 +39,8 @@ class MetaFieldsTable extends AppTable
|
|||
->notEmptyString('value')
|
||||
->notEmptyString('meta_template_id')
|
||||
->notEmptyString('meta_template_field_id')
|
||||
->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_id', 'meta_template_field_id'], 'create');
|
||||
->notEmptyString('meta_template_directory_id')
|
||||
->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_directory_id', ], 'create');
|
||||
|
||||
$validator->add('value', 'validMetaField', [
|
||||
'rule' => 'isValidMetaField',
|
||||
|
@ -46,10 +51,28 @@ class MetaFieldsTable extends AppTable
|
|||
return $validator;
|
||||
}
|
||||
|
||||
public function afterMarshal(EventInterface $event, \App\Model\Entity\MetaField $entity, ArrayObject $data, ArrayObject $options) {
|
||||
if (!isset($entity->meta_template_directory_id)) {
|
||||
$entity->set('meta_template_directory_id', $this->getTemplateDirectoryIdFromMetaTemplate($entity->meta_template_id));
|
||||
}
|
||||
}
|
||||
|
||||
public function getTemplateDirectoryIdFromMetaTemplate($metaTemplateId): int
|
||||
{
|
||||
return $this->MetaTemplates->find()
|
||||
->select('meta_template_directory_id')
|
||||
->where(['id' => $metaTemplateId])
|
||||
->first()
|
||||
->meta_template_directory_id;
|
||||
}
|
||||
|
||||
public function isValidMetaField($value, array $context)
|
||||
{
|
||||
$metaFieldsTable = $context['providers']['table'];
|
||||
$entityData = $context['data'];
|
||||
if (empty($entityData['meta_template_field_id'])) {
|
||||
return true;
|
||||
}
|
||||
$metaTemplateField = $metaFieldsTable->MetaTemplateFields->get($entityData['meta_template_field_id']);
|
||||
return $this->isValidMetaFieldForMetaTemplateField($value, $metaTemplateField);
|
||||
}
|
||||
|
@ -63,14 +86,14 @@ class MetaFieldsTable extends AppTable
|
|||
if (!empty($metaTemplateField['regex'])) {
|
||||
return $this->isValidRegex($value, $metaTemplateField);
|
||||
}
|
||||
if (!empty($metaTemplateField['values_list'])) {
|
||||
return $this->isValidValuesList($value, $metaTemplateField);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isValidType($value, $metaTemplateField)
|
||||
{
|
||||
if (empty($value)) {
|
||||
return __('Metafield value cannot be empty.');
|
||||
}
|
||||
$typeHandler = $this->MetaTemplateFields->getTypeHandler($metaTemplateField['type']);
|
||||
if (!empty($typeHandler)) {
|
||||
$success = $typeHandler->validate($value);
|
||||
|
@ -92,4 +115,11 @@ class MetaFieldsTable extends AppTable
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isValidValuesList($value, $metaTemplateField)
|
||||
{
|
||||
|
||||
$valuesList = $metaTemplateField['values_list'];
|
||||
return in_array($value, $valuesList);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Table;
|
||||
|
||||
use App\Model\Entity\MetaTemplate;
|
||||
use App\Model\Entity\MetaTemplateNameDirectory;
|
||||
use App\Model\Table\AppTable;
|
||||
use Cake\ORM\RulesChecker;
|
||||
use Cake\Validation\Validator;
|
||||
|
||||
class MetaTemplateNameDirectoryTable extends AppTable
|
||||
{
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
$this->hasMany(
|
||||
'MetaFields',
|
||||
[
|
||||
'foreignKey' => 'meta_template_directory_id',
|
||||
]
|
||||
);
|
||||
$this->setDisplayField('name');
|
||||
}
|
||||
|
||||
public function validationDefault(Validator $validator): Validator
|
||||
{
|
||||
$validator
|
||||
->notEmptyString('name')
|
||||
->notEmptyString('namespace')
|
||||
->notEmptyString('uuid')
|
||||
->notEmptyString('version')
|
||||
->requirePresence(['version', 'uuid', 'name', 'namespace'], 'create');
|
||||
return $validator;
|
||||
}
|
||||
|
||||
public function buildRules(RulesChecker $rules): RulesChecker
|
||||
{
|
||||
$rules->add($rules->isUnique(
|
||||
['uuid', 'version'],
|
||||
__('This meta-template already exists.')
|
||||
));
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function createFromMetaTemplate(MetaTemplate $metaTemplate): MetaTemplateNameDirectory
|
||||
{
|
||||
$metaTemplateDirectory = $this->newEntity([
|
||||
'name' => $metaTemplate['name'],
|
||||
'namespace' => $metaTemplate['namespace'],
|
||||
'uuid' => $metaTemplate['uuid'],
|
||||
'version' => $metaTemplate['version'],
|
||||
]);
|
||||
$existingTemplate = $this->find()
|
||||
->where([
|
||||
'uuid' => $metaTemplate['uuid'],
|
||||
'version' => $metaTemplate['version'],
|
||||
])->first();
|
||||
if (!empty($existingTemplate)) {
|
||||
return $existingTemplate;
|
||||
}
|
||||
$this->save($metaTemplateDirectory);
|
||||
return $metaTemplateDirectory;
|
||||
}
|
||||
}
|
|
@ -22,10 +22,15 @@ class MetaTemplatesTable extends AppTable
|
|||
public const UPDATE_STRATEGY_CREATE_NEW = 'create_new';
|
||||
public const UPDATE_STRATEGY_UPDATE_EXISTING = 'update_existing';
|
||||
public const UPDATE_STRATEGY_KEEP_BOTH = 'keep_both';
|
||||
public const UPDATE_STRATEGY_DELETE = 'delete_all';
|
||||
public const UPDATE_STRATEGY_DELETE_ALL = 'delete_all';
|
||||
|
||||
public const DEFAULT_STRATEGY = MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW;
|
||||
public const ALLOWED_STRATEGIES = [MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW];
|
||||
public const DEFAULT_STRATEGY = MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING;
|
||||
public const ALLOWED_STRATEGIES = [
|
||||
MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW,
|
||||
MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING,
|
||||
MetaTemplatesTable::UPDATE_STRATEGY_KEEP_BOTH,
|
||||
MetaTemplatesTable::UPDATE_STRATEGY_DELETE_ALL,
|
||||
];
|
||||
|
||||
private $templatesOnDisk = null;
|
||||
|
||||
|
@ -42,6 +47,9 @@ class MetaTemplatesTable extends AppTable
|
|||
'cascadeCallbacks' => true,
|
||||
]
|
||||
);
|
||||
$this->hasOne('MetaTemplateNameDirectory')
|
||||
->setForeignKey('meta_template_directory_id');
|
||||
|
||||
$this->setDisplayField('name');
|
||||
}
|
||||
|
||||
|
@ -54,7 +62,7 @@ class MetaTemplatesTable extends AppTable
|
|||
->notEmptyString('uuid')
|
||||
->notEmptyString('version')
|
||||
->notEmptyString('source')
|
||||
->requirePresence(['scope', 'source', 'version', 'uuid', 'name', 'namespace'], 'create');
|
||||
->requirePresence(['scope', 'source', 'version', 'uuid', 'name', 'namespace', 'meta_template_directory_id'], 'create');
|
||||
return $validator;
|
||||
}
|
||||
|
||||
|
@ -70,6 +78,41 @@ class MetaTemplatesTable extends AppTable
|
|||
* @return array The update result containing potential errors and the successes
|
||||
*/
|
||||
public function updateAllTemplates(): array
|
||||
{
|
||||
// Create new template found on the disk
|
||||
$newTemplateResult = $this->createAllNewTemplates();
|
||||
$files_processed = $newTemplateResult['files_processed'];
|
||||
$updatesErrors = $newTemplateResult['update_errors'];
|
||||
|
||||
$templatesUpdateStatus = $this->getUpdateStatusForTemplates();
|
||||
|
||||
// Update all existing templates
|
||||
foreach ($templatesUpdateStatus as $uuid => $templateUpdateStatus) {
|
||||
if (!empty($templateUpdateStatus['existing_template'])) {
|
||||
$metaTemplate = $templateUpdateStatus['existing_template'];
|
||||
$result = $this->update($metaTemplate, null);
|
||||
if ($result['success']) {
|
||||
$files_processed[] = $metaTemplate->uuid;
|
||||
}
|
||||
if (!empty($result['errors'])) {
|
||||
$updatesErrors[] = $result['errors'];
|
||||
}
|
||||
}
|
||||
}
|
||||
$results = [
|
||||
'update_errors' => $updatesErrors,
|
||||
'files_processed' => $files_processed,
|
||||
'success' => !empty($files_processed),
|
||||
];
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the templates stored on the disk update and create them in the database without touching at the existing ones
|
||||
*
|
||||
* @return array The update result containing potential errors and the successes
|
||||
*/
|
||||
public function createAllNewTemplates(): array
|
||||
{
|
||||
$updatesErrors = [];
|
||||
$files_processed = [];
|
||||
|
@ -103,7 +146,7 @@ class MetaTemplatesTable extends AppTable
|
|||
* @param string|null $strategy The strategy to be used when updating templates with conflicts
|
||||
* @return array The update result containing potential errors and the successes
|
||||
*/
|
||||
public function update($metaTemplate, $strategy = null): array
|
||||
public function update(\App\Model\Entity\MetaTemplate $metaTemplate, $strategy = null): array
|
||||
{
|
||||
$files_processed = [];
|
||||
$updatesErrors = [];
|
||||
|
@ -111,6 +154,25 @@ class MetaTemplatesTable extends AppTable
|
|||
$templateStatus = $this->getStatusForMetaTemplate($templateOnDisk, $metaTemplate);
|
||||
$updateStatus = $this->computeFullUpdateStatusForMetaTemplate($templateStatus, $metaTemplate);
|
||||
$errors = [];
|
||||
|
||||
$result = $this->doUpdate($updateStatus, $templateOnDisk, $metaTemplate, $strategy);
|
||||
if ($result['success']) {
|
||||
$files_processed[] = $templateOnDisk['uuid'];
|
||||
}
|
||||
if (!empty($result['errors'])) {
|
||||
$updatesErrors[] = implode(', ', Hash::extract($result['errors'], '{n}.message'));
|
||||
}
|
||||
$results = [
|
||||
'update_errors' => $updatesErrors,
|
||||
'files_processed' => $files_processed,
|
||||
'success' => !empty($files_processed),
|
||||
];
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function doUpdate(array $updateStatus, array $templateOnDisk, \App\Model\Entity\MetaTemplate $metaTemplate, string $strategy = null): array
|
||||
{
|
||||
$errors = [];
|
||||
$success = false;
|
||||
if ($updateStatus['up-to-date']) {
|
||||
$errors['message'] = __('Meta-template already up-to-date');
|
||||
|
@ -123,22 +185,17 @@ class MetaTemplatesTable extends AppTable
|
|||
$errors['message'] = __('Cannot update meta-template, update strategy not allowed');
|
||||
} else if (!$updateStatus['up-to-date']) {
|
||||
$strategy = is_null($strategy) ? MetaTemplatesTable::DEFAULT_STRATEGY : $strategy;
|
||||
if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING && !$updateStatus['automatically-updateable']) {
|
||||
$strategy = MetaTemplatesTable::UPDATE_STRATEGY_KEEP_BOTH;
|
||||
}
|
||||
$success = $this->updateMetaTemplateWithStrategyRouter($metaTemplate, $templateOnDisk, $strategy, $errors);
|
||||
} else {
|
||||
$errors['message'] = __('Could not update. Something went wrong.');
|
||||
}
|
||||
if ($success) {
|
||||
$files_processed[] = $templateOnDisk['uuid'];
|
||||
}
|
||||
if (!empty($errors)) {
|
||||
$updatesErrors[] = $errors;
|
||||
}
|
||||
$results = [
|
||||
'update_errors' => $updatesErrors,
|
||||
'files_processed' => $files_processed,
|
||||
'success' => !empty($files_processed),
|
||||
return [
|
||||
'success' => $success,
|
||||
'errors' => $errors,
|
||||
];
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -731,6 +788,8 @@ class MetaTemplatesTable extends AppTable
|
|||
$metaTemplate = $this->newEntity($template, [
|
||||
'associated' => ['MetaTemplateFields']
|
||||
]);
|
||||
$metaTemplateDirectory = $this->MetaTemplateNameDirectory->createFromMetaTemplate($metaTemplate);
|
||||
$metaTemplate->meta_template_directory_id = $metaTemplateDirectory->id;
|
||||
$tmp = $this->save($metaTemplate, [
|
||||
'associated' => ['MetaTemplateFields']
|
||||
]);
|
||||
|
@ -759,13 +818,27 @@ class MetaTemplatesTable extends AppTable
|
|||
$errors[] = new UpdateError(false, $metaTemplate);
|
||||
return false;
|
||||
}
|
||||
$metaTemplate = $this->patchEntity($metaTemplate, $template, [
|
||||
'associated' => ['MetaTemplateFields']
|
||||
]);
|
||||
$metaTemplate = $this->patchEntity($metaTemplate, $template);
|
||||
foreach ($template['metaFields'] as $newMetaField) {
|
||||
$newMetaField['__patched'] = true;
|
||||
foreach($metaTemplate->meta_template_fields as $i => $oldMetaField) {
|
||||
if ($oldMetaField->field == $newMetaField['field']) {
|
||||
$metaTemplate->meta_template_fields[$i] = $this->MetaTemplateFields->patchEntity($oldMetaField, $newMetaField);
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
$metaTemplate->meta_template_fields[] = $this->MetaTemplateFields->newEntity($newMetaField);
|
||||
}
|
||||
$metaTemplate->setDirty('meta_template_fields', true);
|
||||
$metaTemplate = $this->save($metaTemplate, [
|
||||
'associated' => ['MetaTemplateFields']
|
||||
]);
|
||||
if (!empty($metaTemplate)) {
|
||||
foreach ($metaTemplate->meta_template_fields as $savedMetafield) {
|
||||
if (empty($savedMetafield['__patched'])) {
|
||||
$this->MetaTemplateFields->delete($savedMetafield);
|
||||
}
|
||||
}
|
||||
if (empty($metaTemplate)) {
|
||||
$errors[] = new UpdateError(false, __('Could not save the template.'), $metaTemplate->getErrors());
|
||||
return false;
|
||||
}
|
||||
|
@ -792,10 +865,12 @@ class MetaTemplatesTable extends AppTable
|
|||
}
|
||||
if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_KEEP_BOTH) {
|
||||
$result = $this->executeStrategyKeep($template, $metaTemplate);
|
||||
} else if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_DELETE) {
|
||||
$result = $this->executeStrategyDeleteAll($template, $metaTemplate);
|
||||
} else if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING) {
|
||||
$result = $this->updateMetaTemplate($metaTemplate, $template);
|
||||
} else if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW) {
|
||||
$result = $this->executeStrategyCreateNew($template, $metaTemplate);
|
||||
} else if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_DELETE_ALL) {
|
||||
$result = $this->executeStrategyDeleteAll($template, $metaTemplate);
|
||||
} else {
|
||||
$errors[] = new UpdateError(false, __('Invalid strategy {0}', $strategy));
|
||||
return false;
|
||||
|
@ -825,6 +900,7 @@ class MetaTemplatesTable extends AppTable
|
|||
$errors[] = new UpdateError(false, __('Could not save the template. A template with this UUID and version already exists'), ['A template with UUID and version already exists']);
|
||||
}
|
||||
$conflicts = $this->getMetaTemplateConflictsForMetaTemplate($metaTemplate, $template);
|
||||
$conflictingEntities = Hash::combine($conflicts, '{s}.conflictingEntities.{n}.parent_id', '{s}.conflictingEntities.{n}.parent_id');
|
||||
$blockingConflict = Hash::extract($conflicts, '{s}.conflicts');
|
||||
$errors = [];
|
||||
if (empty($blockingConflict)) { // No conflict, everything can be updated without special care
|
||||
|
@ -833,7 +909,6 @@ class MetaTemplatesTable extends AppTable
|
|||
}
|
||||
$entities = $this->getEntitiesHavingMetaFieldsFromTemplate($metaTemplate->id, null);
|
||||
|
||||
$conflictingEntities = [];
|
||||
foreach ($entities as $entity) {
|
||||
$conflicts = $this->getMetaFieldsConflictsUnderTemplate($entity['meta_fields'], $template);
|
||||
if (!empty($conflicts)) {
|
||||
|
@ -844,8 +919,9 @@ class MetaTemplatesTable extends AppTable
|
|||
$this->updateMetaTemplate($metaTemplate, $template, $errors);
|
||||
return !empty($errors) ? $errors[0] : true;
|
||||
}
|
||||
$template['is_default'] = $metaTemplate['is_default'];
|
||||
$template['enabled'] = $metaTemplate['enabled'];
|
||||
$template['is_default'] = $metaTemplate->is_default;
|
||||
$template['enabled'] = $metaTemplate->enabled;
|
||||
$metaTemplate->set('enabled', false);
|
||||
if ($metaTemplate->is_default) {
|
||||
$metaTemplate->set('is_default', false);
|
||||
$this->save($metaTemplate);
|
||||
|
@ -853,13 +929,9 @@ class MetaTemplatesTable extends AppTable
|
|||
$savedMetaTemplate = null;
|
||||
$this->saveNewMetaTemplate($template, $errors, $savedMetaTemplate);
|
||||
if (!empty($savedMetaTemplate)) {
|
||||
$savedMetaTemplateFieldByName = Hash::combine($savedMetaTemplate['meta_template_fields'], '{n}.field', '{n}');
|
||||
foreach ($entities as $entity) {
|
||||
if (empty($conflictingEntities[$entity->id])) { // conflicting entities remain untouched
|
||||
foreach ($entity['meta_fields'] as $metaField) {
|
||||
$savedMetaTemplateField = $savedMetaTemplateFieldByName[$metaField->field];
|
||||
$this->supersedeMetaFieldWithMetaTemplateField($metaField, $savedMetaTemplateField);
|
||||
}
|
||||
$this->supersedeMetaFieldsWithMetaTemplateField($entity['meta_fields'], $savedMetaTemplate);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -868,6 +940,56 @@ class MetaTemplatesTable extends AppTable
|
|||
return true;
|
||||
}
|
||||
|
||||
public function migrateMetafieldsToNewestTemplate(\App\Model\Entity\MetaTemplate $oldMetaTemplate, \App\Model\Entity\MetaTemplate $newestMetaTemplate, bool $forceMigration): array
|
||||
{
|
||||
$result = [
|
||||
'success' => true,
|
||||
'migrated_count' => 0,
|
||||
'conflicting_entities' => 0,
|
||||
'migration_errors' => 0,
|
||||
];
|
||||
|
||||
$entities = $this->getEntitiesHavingMetaFieldsFromTemplate($oldMetaTemplate->id, null);
|
||||
if (empty($entities)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$successfullyMigratedEntities = 0;
|
||||
$migrationErrors = 0;
|
||||
$conflictingEntities = [];
|
||||
foreach ($entities as $entity) {
|
||||
$conflicts = $this->getMetaFieldsConflictsUnderTemplate($entity->meta_fields, $newestMetaTemplate);
|
||||
if (!empty($conflicts)) {
|
||||
$conflictingEntities[] = $entity->id;
|
||||
if (!$forceMigration) {
|
||||
continue;
|
||||
} else {
|
||||
$conflictingMetafieldIDs = Hash::extract($conflicts, '{n}.id');
|
||||
$metaFieldsToDelete = [];
|
||||
foreach ($entity->meta_fields as $i => $metaField) {
|
||||
if (in_array($metaField->id, $conflictingMetafieldIDs)) {
|
||||
$metaFieldsToDelete[] = $metaField;
|
||||
unset($entity->meta_fields[$i]);
|
||||
}
|
||||
}
|
||||
$this->MetaTemplateFields->MetaFields->unlink($entity, $metaFieldsToDelete);
|
||||
}
|
||||
}
|
||||
$success = $this->supersedeMetaFieldsWithMetaTemplateField($entity->meta_fields, $newestMetaTemplate);
|
||||
if ($success) {
|
||||
$successfullyMigratedEntities += 1;
|
||||
} else {
|
||||
$migrationErrors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
$result['success'] = $migrationErrors == 0;
|
||||
$result['migrated_count'] = $successfullyMigratedEntities;
|
||||
$result['conflicting_entities'] = count($conflictingEntities);
|
||||
$result['migration_errors'] = $migrationErrors;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the `delete_all` update strategy by updating the meta-template and deleting all conflicting meta-fields.
|
||||
* Strategy:
|
||||
|
@ -894,9 +1016,7 @@ class MetaTemplatesTable extends AppTable
|
|||
|
||||
foreach ($entities as $entity) {
|
||||
$conflicts = $this->getMetaFieldsConflictsUnderTemplate($entity['meta_fields'], $template);
|
||||
$deletedCount = $this->MetaTemplateFields->MetaFields->deleteAll([
|
||||
'id IN' => $conflicts
|
||||
]);
|
||||
$this->MetaTemplateFields->MetaFields->unlink($entity, $conflicts);
|
||||
}
|
||||
$this->updateMetaTemplate($metaTemplate, $template, $errors);
|
||||
return !empty($errors) ? $errors[0] : true;
|
||||
|
@ -936,16 +1056,20 @@ class MetaTemplatesTable extends AppTable
|
|||
/**
|
||||
* Supersede a meta-fields's meta-template-field with the provided one.
|
||||
*
|
||||
* @param \App\Model\Entity\MetaField $metaField
|
||||
* @param array $metaFields
|
||||
* @param \App\Model\Entity\MetaTemplateField $savedMetaTemplateField
|
||||
* @return bool True if the replacement was a success, False otherwise
|
||||
*/
|
||||
public function supersedeMetaFieldWithMetaTemplateField(\App\Model\Entity\MetaField $metaField, \App\Model\Entity\MetaTemplateField $savedMetaTemplateField): bool
|
||||
public function supersedeMetaFieldsWithMetaTemplateField(array $metaFields, \App\Model\Entity\MetaTemplate $savedMetaTemplate): bool
|
||||
{
|
||||
$metaField->set('meta_template_id', $savedMetaTemplateField->meta_template_id);
|
||||
$metaField->set('meta_template_field_id', $savedMetaTemplateField->id);
|
||||
$metaField = $this->MetaTemplateFields->MetaFields->save($metaField);
|
||||
return !empty($metaField);
|
||||
$savedMetaTemplateFieldByName = Hash::combine($savedMetaTemplate['meta_template_fields'], '{n}.field', '{n}');
|
||||
foreach ($metaFields as $i => $metaField) {
|
||||
$savedMetaTemplateField = $savedMetaTemplateFieldByName[$metaField->field];
|
||||
$metaField->set('meta_template_id', $savedMetaTemplateField->meta_template_id);
|
||||
$metaField->set('meta_template_field_id', $savedMetaTemplateField->id);
|
||||
}
|
||||
$entities = $this->MetaTemplateFields->MetaFields->saveMany($metaFields);
|
||||
return !empty($entities);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -972,13 +1096,14 @@ class MetaTemplatesTable extends AppTable
|
|||
$metaTemplateFieldByName[$metaTemplateField['field']] = $this->MetaTemplateFields->newEntity($metaTemplateField);
|
||||
}
|
||||
foreach ($metaFields as $metaField) {
|
||||
if ($existingMetaTemplate && $metaField->meta_template_id != $template->id) {
|
||||
continue;
|
||||
if (empty($metaTemplateFieldByName[$metaField->field])) { // Meta-field was removed from the template
|
||||
$isValid = false;
|
||||
} else {
|
||||
$isValid = $this->MetaTemplateFields->MetaFields->isValidMetaFieldForMetaTemplateField(
|
||||
$metaField->value,
|
||||
$metaTemplateFieldByName[$metaField->field]
|
||||
);
|
||||
}
|
||||
$isValid = $this->MetaTemplateFields->MetaFields->isValidMetaFieldForMetaTemplateField(
|
||||
$metaField->value,
|
||||
$metaTemplateFieldByName[$metaField->field]
|
||||
);
|
||||
if ($isValid !== true) {
|
||||
$conflicting[] = $metaField;
|
||||
}
|
||||
|
@ -1022,27 +1147,12 @@ class MetaTemplatesTable extends AppTable
|
|||
$result['conflictingEntities'] = Hash::extract($conflictingStatus, '{n}.parent_id');
|
||||
}
|
||||
}
|
||||
if (!empty($templateField['regex']) && $templateField['regex'] != $metaTemplateField->regex) {
|
||||
$entitiesWithMetaFieldQuery = $this->MetaTemplateFields->MetaFields->find();
|
||||
$entitiesWithMetaFieldQuery
|
||||
->enableHydration(false)
|
||||
->select([
|
||||
'parent_id',
|
||||
])
|
||||
->where([
|
||||
'meta_template_field_id' => $metaTemplateField->id,
|
||||
]);
|
||||
$entitiesTable = $this->getTableForMetaTemplateScope($scope);
|
||||
$entities = $entitiesTable->find()
|
||||
->where(['id IN' => $entitiesWithMetaFieldQuery])
|
||||
->contain([
|
||||
'MetaFields' => [
|
||||
'conditions' => [
|
||||
'MetaFields.meta_template_field_id' => $metaTemplateField->id
|
||||
]
|
||||
]
|
||||
])
|
||||
->all()->toList();
|
||||
|
||||
if (
|
||||
(!empty($templateField['regex']) && $templateField['regex'] != $metaTemplateField->regex) ||
|
||||
!empty($templateField['values_list'])
|
||||
) {
|
||||
$entities = $this->getEntitiesForMetaTemplateField($scope, $metaTemplateField->id, true);
|
||||
$conflictingEntities = [];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($entity['meta_fields'] as $metaField) {
|
||||
|
@ -1051,7 +1161,10 @@ class MetaTemplatesTable extends AppTable
|
|||
$templateField
|
||||
);
|
||||
if ($isValid !== true) {
|
||||
$conflictingEntities[] = $entity->id;
|
||||
$conflictingEntities[] = [
|
||||
'parent_id' => $entity->id,
|
||||
'meta_template_field_id' => $metaTemplateField->id,
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1059,13 +1172,50 @@ class MetaTemplatesTable extends AppTable
|
|||
|
||||
if (!empty($conflictingEntities)) {
|
||||
$result['automatically-updateable'] = $result['automatically-updateable'] && false;
|
||||
$result['conflicts'][] = __('This field is instantiated with values not passing the validation anymore');
|
||||
$result['conflicts'][] = __('This field is instantiated with values not passing the validation anymore.');
|
||||
$result['conflictingEntities'] = array_merge($result['conflictingEntities'], $conflictingEntities);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all entities having the meta-fields using the provided meta-template-field.
|
||||
*
|
||||
* @param string $scope
|
||||
* @param integer $metaTemplateFieldID The ID of the matching meta-template-field
|
||||
* @param boolean $includeMatchingMetafields Should the entities also include the matching meta-fields
|
||||
* @return array
|
||||
*/
|
||||
private function getEntitiesForMetaTemplateField(string $scope, int $metaTemplateFieldID, bool $includeMatchingMetafields=true): array
|
||||
{
|
||||
$entitiesTable = $this->getTableForMetaTemplateScope($scope);
|
||||
$entitiesWithMetaFieldQuery = $this->MetaTemplateFields->MetaFields->find();
|
||||
$entitiesWithMetaFieldQuery
|
||||
->enableHydration(false)
|
||||
->select([
|
||||
'parent_id',
|
||||
])
|
||||
->where([
|
||||
'meta_template_field_id' => $metaTemplateFieldID,
|
||||
]);
|
||||
|
||||
$entitiesQuery = $entitiesTable->find()
|
||||
->where(['id IN' => $entitiesWithMetaFieldQuery]);
|
||||
|
||||
if ($includeMatchingMetafields) {
|
||||
$entitiesQuery->contain([
|
||||
'MetaFields' => [
|
||||
'conditions' => [
|
||||
'MetaFields.meta_template_field_id' => $metaTemplateFieldID,
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
return $entitiesQuery->all()->toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the conflict that would be introduced if the metaTemplate would be updated to the provided template
|
||||
*
|
||||
|
@ -1082,7 +1232,7 @@ class MetaTemplatesTable extends AppTable
|
|||
$templateMetaFields = $template['metaFields'];
|
||||
}
|
||||
$conflicts = [];
|
||||
$existingMetaTemplateFields = Hash::combine($metaTemplate->toArray(), 'meta_template_fields.{n}.field');
|
||||
$existingMetaTemplateFields = Hash::combine($metaTemplate->toArray(), 'meta_template_fields.{n}.field', 'meta_template_fields.{n}');
|
||||
foreach ($templateMetaFields as $newMetaField) {
|
||||
foreach ($metaTemplate->meta_template_fields as $metaField) {
|
||||
if ($newMetaField['field'] == $metaField->field) {
|
||||
|
@ -1098,10 +1248,23 @@ class MetaTemplatesTable extends AppTable
|
|||
}
|
||||
}
|
||||
if (!empty($existingMetaTemplateFields)) {
|
||||
foreach ($existingMetaTemplateFields as $field => $tmp) {
|
||||
$conflicts[$field] = [
|
||||
foreach ($existingMetaTemplateFields as $metaTemplateField) {
|
||||
$query = $this->MetaTemplateFields->MetaFields->find();
|
||||
$query
|
||||
->enableHydration(false)
|
||||
->select([
|
||||
'parent_id',
|
||||
'meta_template_field_id',
|
||||
])
|
||||
->where([
|
||||
'meta_template_field_id' => $metaTemplateField['id'],
|
||||
])
|
||||
->group(['parent_id']);
|
||||
$entityWithMetafieldToBeRemoved = $query->all()->toList();
|
||||
$conflicts[$metaTemplateField['field']] = [
|
||||
'automatically-updateable' => false,
|
||||
'conflicts' => [__('This field is intended to be removed')],
|
||||
'conflictingEntities' => $entityWithMetafieldToBeRemoved,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -1154,9 +1317,9 @@ class MetaTemplatesTable extends AppTable
|
|||
$updateStatus['current_version'] = $metaTemplate->version;
|
||||
$updateStatus['next_version'] = $template['version'];
|
||||
$updateStatus['new'] = false;
|
||||
if ($metaTemplate->version >= $template['version']) {
|
||||
$updateStatus['automatically-updateable'] = false;
|
||||
if (intval($metaTemplate->version) >= intval($template['version'])) {
|
||||
$updateStatus['up-to-date'] = true;
|
||||
$updateStatus['automatically-updateable'] = false;
|
||||
$updateStatus['conflicts'][] = __('Could not update the template. Local version is equal or newer.');
|
||||
return $updateStatus;
|
||||
}
|
||||
|
@ -1164,6 +1327,17 @@ class MetaTemplatesTable extends AppTable
|
|||
$conflicts = $this->getMetaTemplateConflictsForMetaTemplate($metaTemplate, $template);
|
||||
if (!empty($conflicts)) {
|
||||
$updateStatus['conflicts'] = $conflicts;
|
||||
$updateStatus['automatically-updateable'] = false;
|
||||
$emptySum = 0;
|
||||
foreach ($conflicts as $fieldname => $fieldStatus) {
|
||||
if (!empty($fieldStatus['conflictingEntities'])) {
|
||||
break;
|
||||
}
|
||||
$emptySum += 1;
|
||||
}
|
||||
if ($emptySum == count($conflicts)) {
|
||||
$updateStatus['automatically-updateable'] = true;
|
||||
}
|
||||
} else {
|
||||
$updateStatus['automatically-updateable'] = true;
|
||||
}
|
||||
|
|
|
@ -91,10 +91,6 @@ class PermissionLimitationsTable extends AppTable
|
|||
foreach ($metaTemplate['meta_template_fields'] as &$meta_template_field) {
|
||||
$boolean = $meta_template_field['type'] === 'boolean';
|
||||
foreach ($meta_template_field['metaFields'] as &$metaField) {
|
||||
if ($boolean) {
|
||||
$metaField['value'] = '<i class="fas fa-' . ((bool)$metaField['value'] ? 'check' : 'times') . '"></i>';
|
||||
$metaField['no_escaping'] = true;
|
||||
}
|
||||
if (isset($permissionLimitations[$metaField['field']])) {
|
||||
foreach ($permissionLimitations[$metaField['field']] as $scope => $value) {
|
||||
$messageType = 'warning';
|
||||
|
|
|
@ -342,7 +342,13 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
|
|||
'type' => 'boolean',
|
||||
'description' => __('This setting will enforce that usernames conform to basic requirements of e-mail addresses.'),
|
||||
'default' => false
|
||||
]
|
||||
],
|
||||
'user.allow-user-deletion' => [
|
||||
'name' => __('Allow user deletion'),
|
||||
'type' => 'boolean',
|
||||
'description' => __('This setting will allow the deletion of users by authorized users.'),
|
||||
'default' => false
|
||||
],
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace App\Utility\Utils;
|
||||
|
||||
// src: https://www.php.net/manual/en/function.array-diff.php#91756
|
||||
function array_diff_recursive($arr1, $arr2)
|
||||
{
|
||||
$outputDiff = [];
|
||||
|
||||
foreach ($arr1 as $key => $value) {
|
||||
//if the key exists in the second array, recursively call this function
|
||||
//if it is an array, otherwise check if the value is in arr2
|
||||
if (array_key_exists($key, $arr2)) {
|
||||
if (is_array($value)) {
|
||||
$recursiveDiff = array_diff_recursive($value, $arr2[$key]);
|
||||
|
||||
if (count($recursiveDiff)) {
|
||||
$outputDiff[$key] = $recursiveDiff;
|
||||
}
|
||||
} else if (!in_array($value, $arr2)) {
|
||||
$outputDiff[$key] = $value;
|
||||
}
|
||||
}
|
||||
//if the key is not in the second array, check if the value is in
|
||||
//the second array (this is a quirk of how array_diff works)
|
||||
else if (!in_array($value, $arr2)) {
|
||||
$outputDiff[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $outputDiff;
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
use App\View\Helper\BootstrapHelper;
|
||||
|
||||
/**
|
||||
* Creates an collapsible accordion component
|
||||
*
|
||||
* # Options:
|
||||
* - stayOpen: Should collapsible components stay open when another one is opened
|
||||
* - class: Additional classes to add to the main accordion container
|
||||
* - content: Definition of the collapsible components. Must have at least the $body key set. See the "# Content" section for the options
|
||||
*
|
||||
* # Content:
|
||||
* - class: Additional class to add to the body container
|
||||
* - open: Should that collapsible element be opened by default
|
||||
* - variant: The background variant to be applied to the body element
|
||||
* - header: The definition of the interactive header. Accepts the following options:
|
||||
* - variant: The bootstrap variant to apply on the header element
|
||||
* - text: The text content of the header
|
||||
* - html: The HTML content of the header
|
||||
*
|
||||
* # Usage:
|
||||
* $this->Bootstrap->accordion(
|
||||
* [
|
||||
* 'stayOpen' => true,
|
||||
* ],
|
||||
* [
|
||||
* [
|
||||
* 'open' => true,
|
||||
* 'header' => [
|
||||
* 'variant' => 'danger',
|
||||
* 'text' => 'nav 1',
|
||||
* ],
|
||||
* 'body' => '<b>body</b>',
|
||||
* ],
|
||||
* [
|
||||
* 'class' => ['opacity-50'],
|
||||
* 'variant' => 'success',
|
||||
* 'header' => [
|
||||
* 'html' => '<i>nav 1</i>',
|
||||
* ],
|
||||
* 'body' => '<b>body</b>',
|
||||
* ],
|
||||
* ]
|
||||
* );
|
||||
*/
|
||||
class BootstrapAccordion extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'stayOpen' => false,
|
||||
'class' => [],
|
||||
];
|
||||
|
||||
function __construct(array $options, array $content, BootstrapHelper $btHelper)
|
||||
{
|
||||
$this->allowedOptionValues = [];
|
||||
$this->content = $content;
|
||||
$this->btHelper = $btHelper;
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->checkOptionValidity();
|
||||
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||
$this->seed = 'acc-' . mt_rand();
|
||||
$this->contentSeeds = [];
|
||||
foreach ($this->content as $accordionItem) {
|
||||
$this->contentSeeds[] = mt_rand();
|
||||
}
|
||||
|
||||
foreach ($this->content as $i => $item) {
|
||||
$this->content[$i]['class'] = $this->convertToArrayIfNeeded($item['class'] ?? []);
|
||||
$this->content[$i]['header']['class'] = $this->convertToArrayIfNeeded($item['header']['class'] ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
public function accordion(): string
|
||||
{
|
||||
return $this->genAccordion();
|
||||
}
|
||||
|
||||
private function genHeader(array $accordionItem, int $i): string
|
||||
{
|
||||
$html = $this->nodeOpen('h2', [
|
||||
'class' => ['accordion-header'],
|
||||
'id' => 'head-' . $this->contentSeeds[$i]
|
||||
]);
|
||||
$content = $accordionItem['header']['html'] ?? h($accordionItem['header']['text']);
|
||||
$buttonOptions = [
|
||||
'class' => array_merge(
|
||||
[
|
||||
'accordion-button',
|
||||
empty($accordionItem['open']) ? 'collapsed' : '',
|
||||
self::getBGAndTextClassForVariant($accordionItem['header']['variant'] ?? ''),
|
||||
],
|
||||
$accordionItem['header']['class'],
|
||||
),
|
||||
'type' => 'button',
|
||||
'data-bs-toggle' => 'collapse',
|
||||
'data-bs-target' => '#body-' . $this->contentSeeds[$i],
|
||||
'aria-expanded' => 'false',
|
||||
'aria-controls' => 'body-' . $this->contentSeeds[$i],
|
||||
];
|
||||
$html .= $this->node('button', $buttonOptions, $content);
|
||||
$html .= $this->nodeClose(('h2'));
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genBody(array $accordionItem, int $i): string
|
||||
{
|
||||
$content = $this->node('div', [
|
||||
'class' => ['accordion-body']
|
||||
], $accordionItem['body']);
|
||||
$divOptions = [
|
||||
'class' => array_merge(
|
||||
[
|
||||
'accordion-collapse collapse',
|
||||
empty($accordionItem['open']) ? '' : 'show',
|
||||
self::getBGAndTextClassForVariant($accordionItem['variant'] ?? ''),
|
||||
],
|
||||
$accordionItem['class'],
|
||||
),
|
||||
'id' => 'body-' . $this->contentSeeds[$i],
|
||||
'aria-labelledby' => 'head-' . $this->contentSeeds[$i],
|
||||
];
|
||||
if (empty($this->options['stayOpen'])) {
|
||||
$divOptions['data-bs-parent'] = '#' . $this->seed;
|
||||
}
|
||||
$html = $this->node('div', $divOptions, $content);
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genAccordion(): string
|
||||
{
|
||||
$html = $this->nodeOpen('div', [
|
||||
'class' => array_merge(['accordion'], $this->options['class']),
|
||||
'id' => $this->seed
|
||||
]);
|
||||
foreach ($this->content as $i => $accordionItem) {
|
||||
$html .= $this->nodeOpen('div', [
|
||||
'class' => array_merge(['accordion-item'])
|
||||
]);
|
||||
$html .= $this->genHeader($accordionItem, $i);
|
||||
$html .= $this->genBody($accordionItem, $i);
|
||||
$html .= $this->nodeClose('div');
|
||||
}
|
||||
$html .= $this->nodeClose('div');
|
||||
return $html;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
|
||||
/**
|
||||
* Creates a bootstrap alert
|
||||
*
|
||||
* # Options:
|
||||
* - text: The text content of the alert
|
||||
* - html: The HTML content of the alert
|
||||
* - dismissible: Can the alert be dissmissed
|
||||
* - variant: The Bootstrap variant of the alert
|
||||
* - fade: Should the alert fade when dismissed
|
||||
* - class: Additional classes to add to the alert container
|
||||
*
|
||||
* # Usage:
|
||||
* $this->Bootstrap->alert([
|
||||
* 'text' => 'This is an alert',
|
||||
* 'dismissible' => false,
|
||||
* 'variant' => 'warning',
|
||||
* 'fade' => false,
|
||||
* ]);
|
||||
*/
|
||||
class BootstrapAlert extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'text' => '',
|
||||
'html' => null,
|
||||
'dismissible' => true,
|
||||
'variant' => 'primary',
|
||||
'fade' => true,
|
||||
'class' => [],
|
||||
];
|
||||
|
||||
function __construct(array $options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => BootstrapGeneric::$variants,
|
||||
];
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||
$this->checkOptionValidity();
|
||||
}
|
||||
|
||||
public function alert(): string
|
||||
{
|
||||
return $this->genAlert();
|
||||
}
|
||||
|
||||
private function genAlert(): string
|
||||
{
|
||||
$html = $this->nodeOpen('div', [
|
||||
'class' => array_merge([
|
||||
'alert',
|
||||
"alert-{$this->options['variant']}",
|
||||
$this->options['dismissible'] ? 'alert-dismissible' : '',
|
||||
$this->options['fade'] ? 'fade show' : '',
|
||||
], $this->options['class']),
|
||||
'role' => "alert"
|
||||
]);
|
||||
|
||||
$html .= $this->options['html'] ?? h($this->options['text']);
|
||||
$html .= $this->genCloseButton();
|
||||
$html .= $this->nodeClose('div');
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genCloseButton(): string
|
||||
{
|
||||
$html = '';
|
||||
if ($this->options['dismissible']) {
|
||||
$html .= $this->genericCloseButton('alert');
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a Bootstrap badge
|
||||
*
|
||||
* # Options:
|
||||
* - text: The text content of the badge
|
||||
* - html: The HTML content of the badge
|
||||
* - variant: The Bootstrap variant of the badge
|
||||
* - pill: Should the badge have a Bootstrap pill style
|
||||
* - icon: Should the button have an icon right before the text
|
||||
* - title: The title of the badge
|
||||
* - class: Additional class to add to the button
|
||||
*
|
||||
* # Usage:
|
||||
* echo $this->Bootstrap->badge([
|
||||
* 'text' => 'text',
|
||||
* 'variant' => 'success',
|
||||
* 'pill' => false,
|
||||
* ]);
|
||||
*/
|
||||
class BootstrapBadge extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'id' => '',
|
||||
'text' => '',
|
||||
'html' => null,
|
||||
'variant' => 'primary',
|
||||
'pill' => false,
|
||||
'icon' => false,
|
||||
'title' => '',
|
||||
'class' => [],
|
||||
];
|
||||
|
||||
function __construct(array $options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => BootstrapGeneric::$variants,
|
||||
];
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||
$this->checkOptionValidity();
|
||||
}
|
||||
|
||||
public function badge(): string
|
||||
{
|
||||
return $this->genBadge();
|
||||
}
|
||||
|
||||
private function genBadge(): string
|
||||
{
|
||||
$html = $this->node('span', [
|
||||
'class' => array_merge($this->options['class'], [
|
||||
'ms-1',
|
||||
'badge',
|
||||
self::getBGAndTextClassForVariant($this->options['variant']),
|
||||
$this->options['pill'] ? 'rounded-pill' : '',
|
||||
]),
|
||||
'title' => $this->options['title'],
|
||||
'id' => $this->options['id'] ?? '',
|
||||
], [
|
||||
$this->genIcon(),
|
||||
$this->options['html'] ?? h($this->options['text'])
|
||||
]);
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genIcon(): string
|
||||
{
|
||||
if (!empty($this->options['icon'])) {
|
||||
$bsIcon = new BootstrapIcon($this->options['icon'], [
|
||||
'class' => [(!empty($this->options['text']) ? 'me-1' : '')]
|
||||
]);
|
||||
return $bsIcon->icon();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
|
||||
/**
|
||||
* Creates a bootstrap button
|
||||
*
|
||||
* # Options:
|
||||
* - text: The text content of the button
|
||||
* - html: The HTML content of the button
|
||||
* - variant: The Bootstrap variant of the button
|
||||
* - outline: Should the button be outlined
|
||||
* - size: The size of the button. Accepts 'xs', 'sm', 'lg'. Leave empty for normal size
|
||||
* - icon: Should the button have an icon right before the text
|
||||
* - image: Should the button have an image in place of an icon right before the text
|
||||
* - class: Additional class to add to the button
|
||||
* - type: The HTML type of the button for forms. Accepts: 'button' (default), 'submit', and 'reset'
|
||||
* - nodeType: Allow to use a different HTML tag than 'button'
|
||||
* - title: The button title
|
||||
* - Badge: Should the button have a badge. Accepts a \BootstrapElement\BootstrapBadge configuration object
|
||||
* - onclick: Shorthand to add a onclick listener function
|
||||
* - attrs: Additional HTML attributes
|
||||
*
|
||||
* # Usage:
|
||||
* $this->Bootstrap->button([
|
||||
* 'text' => 'Press me!',
|
||||
* 'variant' => 'warning',
|
||||
* 'icon' => 'exclamation-triangle',
|
||||
* 'onclick' => 'alert(1)',
|
||||
* ]);
|
||||
*/
|
||||
class BootstrapButton extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'id' => '',
|
||||
'text' => '',
|
||||
'html' => null,
|
||||
'variant' => 'primary',
|
||||
'outline' => false,
|
||||
'size' => '',
|
||||
'icon' => null,
|
||||
'image' => null,
|
||||
'class' => [],
|
||||
'type' => 'button',
|
||||
'nodeType' => 'button',
|
||||
'title' => '',
|
||||
'badge' => false,
|
||||
'onclick' => false,
|
||||
'attrs' => [],
|
||||
];
|
||||
|
||||
private $bsClasses = [];
|
||||
|
||||
function __construct(array $options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => array_merge(BootstrapGeneric::$variants, ['link', 'text']),
|
||||
'size' => ['', 'xs', 'sm', 'lg'],
|
||||
'type' => ['button', 'submit', 'reset']
|
||||
];
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||
$this->checkOptionValidity();
|
||||
|
||||
if (!empty($this->options['id'])) {
|
||||
$this->options['attrs']['id'] = $this->options['id'];
|
||||
}
|
||||
|
||||
$this->bsClasses[] = 'btn';
|
||||
if ($this->options['outline']) {
|
||||
$this->bsClasses[] = "btn-outline-{$this->options['variant']}";
|
||||
} else {
|
||||
$this->bsClasses[] = "btn-{$this->options['variant']}";
|
||||
}
|
||||
if (!empty($this->options['size'])) {
|
||||
$this->bsClasses[] = "btn-{$this->options['size']}";
|
||||
}
|
||||
if ($this->options['variant'] == 'text') {
|
||||
$this->bsClasses[] = 'p-0';
|
||||
$this->bsClasses[] = 'lh-1';
|
||||
}
|
||||
if (!empty($this->options['onclick'])) {
|
||||
$this->options['attrs']['onclick'] = $this->options['onclick'];
|
||||
}
|
||||
}
|
||||
|
||||
public function button(): string
|
||||
{
|
||||
return $this->genButton();
|
||||
}
|
||||
|
||||
private function genButton(): string
|
||||
{
|
||||
$html = $this->nodeOpen($this->options['nodeType'], array_merge($this->options['attrs'], [
|
||||
'class' => array_merge($this->options['class'], $this->bsClasses),
|
||||
'role' => "alert",
|
||||
'type' => $this->options['type'],
|
||||
'title' => h($this->options['title']),
|
||||
]));
|
||||
|
||||
$html .= $this->genIcon();
|
||||
$html .= $this->genImage();
|
||||
$html .= $this->options['html'] ?? h($this->options['text']);
|
||||
if (!empty($this->options['badge'])) {
|
||||
$bsBadge = new BootstrapBadge($this->options['badge']);
|
||||
$html .= $bsBadge->badge();
|
||||
}
|
||||
$html .= $this->nodeClose($this->options['nodeType']);
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genIcon(): string
|
||||
{
|
||||
if (!empty($this->options['icon'])) {
|
||||
$bsIcon = new BootstrapIcon($this->options['icon'], [
|
||||
'class' => [(!empty($this->options['text']) ? 'me-1' : '')]
|
||||
]);
|
||||
return $bsIcon->icon();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function genImage(): string
|
||||
{
|
||||
if (!empty($this->options['image'])) {
|
||||
return $this->node('img', [
|
||||
'src' => $this->options['image']['path'] ?? '',
|
||||
'class' => ['img-fluid', 'me-1'],
|
||||
'width' => '26',
|
||||
'height' => '26',
|
||||
'alt' => $this->options['image']['alt'] ?? ''
|
||||
]);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
|
||||
/**
|
||||
* Creates a Bootstrap card with the given options
|
||||
*
|
||||
* # Options:
|
||||
* - headerText, bodyText, footerText: The text for the mentioned card component
|
||||
* - headerHTML, bodyHTML, footerHtml: The HTML for the mentioned card component
|
||||
* - class: A list of additional class to be added to the main container
|
||||
* - headerVariant, bodyVariant, footerVariant: The variant for the mentioned card component
|
||||
* - headerClass, bodyClass, footerClass: A list of additional class to be added to the main container
|
||||
*
|
||||
* # Usage:
|
||||
* $this->Bootstrap->card([
|
||||
* 'headerText' => 'header',
|
||||
* 'bodyHTML' => '<i>body</i>',
|
||||
* 'footerText' => 'footer',
|
||||
* 'headerVariant' => 'warning',
|
||||
* 'footerVariant' => 'dark',
|
||||
* );
|
||||
*/
|
||||
class BootstrapCard extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'headerText' => '',
|
||||
'bodyText' => '',
|
||||
'footerText' => '',
|
||||
'headerHTML' => null,
|
||||
'bodyHTML' => null,
|
||||
'footerHTML' => null,
|
||||
'class' => [],
|
||||
'headerVariant' => '',
|
||||
'bodyVariant' => '',
|
||||
'footerVariant' => '',
|
||||
'headerClass' => '',
|
||||
'bodyClass' => '',
|
||||
'footerClass' => '',
|
||||
];
|
||||
|
||||
public function __construct(array $options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'headerVariant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||
'bodyVariant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||
'footerVariant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||
];
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->options['headerClass'] = $this->convertToArrayIfNeeded($this->options['headerClass']);
|
||||
$this->options['bodyClass'] = $this->convertToArrayIfNeeded($this->options['bodyClass']);
|
||||
$this->options['footerClass'] = $this->convertToArrayIfNeeded($this->options['footerClass']);
|
||||
$this->checkOptionValidity();
|
||||
$this->options['borderVariant'] = !empty($this->options['headerVariant']) ? "border-{$this->options['headerVariant']}" : '';
|
||||
}
|
||||
|
||||
public function card(): string
|
||||
{
|
||||
return $this->genCard();
|
||||
}
|
||||
|
||||
private function genCard(): string
|
||||
{
|
||||
$card = $this->node('div', [
|
||||
'class' => array_merge(
|
||||
[
|
||||
'card',
|
||||
$this->options['borderVariant'],
|
||||
],
|
||||
$this->options['class']
|
||||
),
|
||||
], implode('', [$this->genHeader(), $this->genBody(), $this->genFooter()]));
|
||||
return $card;
|
||||
}
|
||||
|
||||
private function genHeader(): string
|
||||
{
|
||||
if (empty($this->options['headerHTML']) && empty($this->options['headerText'])) {
|
||||
return '';
|
||||
}
|
||||
$content = $this->options['headerHTML'] ?? h($this->options['headerText']);
|
||||
$header = $this->node('div', [
|
||||
'class' => array_merge(
|
||||
[
|
||||
'card-header',
|
||||
self::getBGAndTextClassForVariant($this->options['headerVariant']),
|
||||
],
|
||||
$this->options['headerClass']
|
||||
),
|
||||
], $content);
|
||||
return $header;
|
||||
}
|
||||
|
||||
private function genBody(): string
|
||||
{
|
||||
if (empty($this->options['bodyHTML']) && empty($this->options['bodyText'])) {
|
||||
return '';
|
||||
}
|
||||
$content = $this->options['bodyHTML'] ?? h($this->options['bodyText']);
|
||||
$body = $this->node('div', [
|
||||
'class' => array_merge(
|
||||
[
|
||||
'card-body',
|
||||
self::getBGAndTextClassForVariant($this->options['bodyVariant']),
|
||||
],
|
||||
$this->options['bodyClass']
|
||||
)
|
||||
], $content);
|
||||
return $body;
|
||||
}
|
||||
|
||||
private function genFooter(): string
|
||||
{
|
||||
if (empty($this->options['footerHTML']) && empty($this->options['footerText'])) {
|
||||
return '';
|
||||
}
|
||||
$content = $this->options['footerHTML'] ?? h($this->options['footerText']);
|
||||
$footer = $this->node('div', [
|
||||
'class' => array_merge([
|
||||
'card-footer',
|
||||
self::getBGAndTextClassForVariant($this->options['footerVariant']),
|
||||
],
|
||||
$this->options['footerClass']
|
||||
)
|
||||
], $content);
|
||||
return $footer;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use Cake\Utility\Security;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
use App\View\Helper\BootstrapHelper;
|
||||
|
||||
/**
|
||||
* Creates a Bootstrap collapsible component
|
||||
*
|
||||
* # Options:
|
||||
* - text: The text of the control element
|
||||
* - html: The HTML content of the control element
|
||||
* - open: Should the collapsible element be opened by default
|
||||
* - horizontal: Should the collapsible be revealed from the side
|
||||
* - class: List of additional classes to be added to the main container
|
||||
* - id: Optional ID to link the collapsible element with its control button
|
||||
* - button: Configuration object to make the control element into a button. Accepts BootstrapElements\BootstrapButton parameters
|
||||
* - card: Configuration object to adjust the content container based on configuration. Accepts BootstrapElements\BootstrapCard parameters
|
||||
*
|
||||
* # Usage:
|
||||
* $this->Bootstrap->collapse([
|
||||
* 'button' => [
|
||||
* 'text' => 'Open sesame',
|
||||
* 'variant' => 'success',
|
||||
* ],
|
||||
* 'card' => [
|
||||
* 'bodyClass' => 'p-2 rounded-3',
|
||||
* 'bodyVariant' => 'secondary',
|
||||
* ]
|
||||
* ], '<i>content</i>');
|
||||
*/
|
||||
|
||||
class BootstrapCollapse extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'text' => '',
|
||||
'html' => null,
|
||||
'open' => false,
|
||||
'horizontal' => false,
|
||||
'class' => [],
|
||||
'button' => [],
|
||||
'card' => false,
|
||||
'attrs' => [],
|
||||
];
|
||||
|
||||
function __construct(array $options, string $content, BootstrapHelper $btHelper)
|
||||
{
|
||||
$this->allowedOptionValues = [];
|
||||
$this->processOptions($options);
|
||||
$this->content = $content;
|
||||
$this->btHelper = $btHelper;
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||
$this->options['class'][] = 'collapse';
|
||||
if (!empty($this->options['horizontal'])) {
|
||||
$this->options['class'][] = 'collapse-horizontal';
|
||||
}
|
||||
if ($this->options['open']) {
|
||||
$this->options['class'][] = 'show';
|
||||
}
|
||||
if ($this->options['card'] !== false && empty($this->options['card']['bodyClass'])) {
|
||||
$this->options['card']['bodyClass'] = ['p-0'];
|
||||
}
|
||||
if (empty($this->options['id'])) {
|
||||
$this->options['id'] = 'c-' . Security::randomString(8);
|
||||
}
|
||||
$this->checkOptionValidity();
|
||||
}
|
||||
|
||||
public function collapse(): string
|
||||
{
|
||||
return $this->genCollapse();
|
||||
}
|
||||
|
||||
private function genControl(): string
|
||||
{
|
||||
$attrsConfig = [
|
||||
'data-bs-toggle' => 'collapse',
|
||||
'role' => 'button',
|
||||
'aria-expanded' => 'false',
|
||||
'aria-controls' => $this->options['id'],
|
||||
'href' => '#' . $this->options['id'],
|
||||
];
|
||||
$html = '';
|
||||
if (!empty($this->options['button'])) {
|
||||
$btnConfig = array_merge($this->options['button'], ['attrs' => $attrsConfig]);
|
||||
$html = $this->btHelper->button($btnConfig);
|
||||
} else {
|
||||
$nodeConfig = [
|
||||
'class' => ['text-decoration-none'],
|
||||
];
|
||||
$nodeConfig = array_merge($nodeConfig, $attrsConfig);
|
||||
$html = $this->node('a', $nodeConfig, $this->options['html'] ?? h($this->options['text']));
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genContent(): string
|
||||
{
|
||||
if (!empty($this->options['card'])) {
|
||||
$cardConfig = $this->options['card'];
|
||||
$cardConfig['bodyHTML'] = $this->content;
|
||||
$content = $this->btHelper->card($cardConfig);
|
||||
} else {
|
||||
$content = $this->content;
|
||||
}
|
||||
$container = $this->node('div', [
|
||||
'class' => $this->options['class'],
|
||||
'id' => $this->options['id'],
|
||||
], $content);
|
||||
return $container;
|
||||
}
|
||||
|
||||
private function genCollapse(): string
|
||||
{
|
||||
return $this->genControl() . $this->genContent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
use App\View\Helper\BootstrapHelper;
|
||||
|
||||
/**
|
||||
* # Options
|
||||
* - dropdown-class: Class for the dropdown
|
||||
* - alignment: How should the dropdown be aligned. Valid: "start", "end"
|
||||
* - direction: Position where the dropdown will be displayed Valid: "start", "end", "up", "down"
|
||||
* - button: Configuration for the dropdown button to be passed to BootstrapElements\BootstrapButton
|
||||
* - submenu_alignment: Alignment of the child dropdown will be displayed Valid: "start", "end", "up", "down"
|
||||
* - submenu_direction: Position where the child dropdown will be displayed Valid: "start", "end", "up", "down"
|
||||
* - attrs: Additional HTML attributes to be applied on the dropdown container
|
||||
* - menu: Entries making the dropdown menu. Accept the following options:
|
||||
* - text: Text of the entry
|
||||
* - html: HTML of the entry
|
||||
* - icon: Icon displayed before the text
|
||||
* - badge: Badge displayed after the text. Accepts BootstrapElements\BootstrapBadge
|
||||
* - header: Is this item a list header
|
||||
* - keepOpen: Keep the dropdown open if this entry is clicked
|
||||
* - sup: Additional text to be added as a <sup> element
|
||||
* - attrs: Additional HTML attributes to be applied on the entry
|
||||
*
|
||||
* # Usage:
|
||||
* $this->Bootstrap->dropdownMenu([
|
||||
* 'dropdown-class' => 'ms-1',
|
||||
* 'alignment' => 'end',
|
||||
* 'direction' => 'down',
|
||||
* 'button' => [
|
||||
* 'icon' => 'sliders-h',
|
||||
* 'variant' => 'primary',
|
||||
* ],
|
||||
* 'submenu_alignment' => 'end',
|
||||
* 'submenu_direction' => 'end',
|
||||
* 'attrs' => [],
|
||||
* 'menu' => [
|
||||
* [
|
||||
* 'text' => __('Eye'),
|
||||
* 'icon' => 'eye-slash',
|
||||
* 'keepOpen' => true,
|
||||
* 'menu' => [
|
||||
* ['header' => true, 'text' => 'nested menu'],
|
||||
* ['text' => 'item 1'],
|
||||
* ['text' => 'item 2', 'sup' => 'v1'],
|
||||
* ],
|
||||
* ],
|
||||
* [
|
||||
* 'html' => '<i class="p-3">html item</i>',
|
||||
* ],
|
||||
* ]
|
||||
* ]);
|
||||
*/
|
||||
|
||||
class BootstrapDropdownMenu extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'dropdown-class' => [],
|
||||
'alignment' => 'start',
|
||||
'direction' => 'end',
|
||||
'button' => [],
|
||||
'menu' => [],
|
||||
'submenu_direction' => 'end',
|
||||
'submenu_classes' => [],
|
||||
'attrs' => [],
|
||||
];
|
||||
|
||||
function __construct(array $options, BootstrapHelper $btHelper)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'direction' => ['start', 'end', 'up', 'down'],
|
||||
'alignment' => ['start', 'end'],
|
||||
'submenu_direction' => ['start', 'end', 'up', 'down'],
|
||||
];
|
||||
$this->processOptions($options);
|
||||
$this->menu = $this->options['menu'];
|
||||
$this->btHelper = $btHelper;
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->options['dropdown-class'] = $this->convertToArrayIfNeeded($this->options['dropdown-class']);
|
||||
$this->checkOptionValidity();
|
||||
}
|
||||
|
||||
public function dropdownMenu(): string
|
||||
{
|
||||
return $this->fullDropdown();
|
||||
}
|
||||
|
||||
public function fullDropdown(): string
|
||||
{
|
||||
return $this->genDropdownWrapper($this->genDropdownToggleButton(), $this->genDropdownMenu($this->menu));
|
||||
}
|
||||
|
||||
public function genDropdownWrapper(string $toggle = '', string $menu = '', $direction = null, $classes = null): string
|
||||
{
|
||||
$classes = !is_null($classes) ? $classes : $this->options['dropdown-class'];
|
||||
$direction = !is_null($direction) ? $direction : $this->options['direction'];
|
||||
$content = $toggle . $menu;
|
||||
$html = $this->node('div', array_merge(
|
||||
$this->options['attrs'],
|
||||
[
|
||||
'class' => array_merge(
|
||||
$classes,
|
||||
[
|
||||
'dropdown',
|
||||
"drop{$direction}"
|
||||
]
|
||||
)
|
||||
]
|
||||
), $content);
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function genDropdownToggleButton(): string
|
||||
{
|
||||
$defaultOptions = [
|
||||
'class' => ['dropdown-toggle'],
|
||||
'attrs' => [
|
||||
'data-bs-toggle' => 'dropdown',
|
||||
'aria-expanded' => 'false',
|
||||
]
|
||||
];
|
||||
$options = array_merge_recursive($this->options['button'], $defaultOptions);
|
||||
return $this->btHelper->button($options);
|
||||
}
|
||||
|
||||
private function genDropdownMenu(array $entries, $alignment = null): string
|
||||
{
|
||||
$alignment = !is_null($alignment) ? $alignment : $this->options['alignment'];
|
||||
$html = $this->node('div', [
|
||||
'class' => ['dropdown-menu', "dropdown-menu-{$alignment}"],
|
||||
], $this->genEntries($entries));
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genEntries(array $entries): string
|
||||
{
|
||||
$html = '';
|
||||
foreach ($entries as $entry) {
|
||||
$link = $this->genEntry($entry);
|
||||
if (!empty($entry['menu'])) {
|
||||
$html .= $this->genDropdownWrapper($link, $this->genDropdownMenu($entry['menu']), $this->options['submenu_direction'], $this->options['submenu_classes']);
|
||||
} else {
|
||||
$html .= $link;
|
||||
}
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genEntry(array $entry): string
|
||||
{
|
||||
if (!empty($entry['html'])) {
|
||||
return $entry['html'];
|
||||
}
|
||||
$classes = [];
|
||||
$icon = '';
|
||||
if (!empty($entry['icon'])) {
|
||||
$icon = $this->btHelper->icon($entry['icon'], ['class' => 'me-2']);
|
||||
}
|
||||
$badge = '';
|
||||
if (!empty($entry['badge'])) {
|
||||
$bsBadge = new BootstrapBadge(array_merge(
|
||||
['class' => ['ms-auto']],
|
||||
$entry['badge']
|
||||
));
|
||||
$badge = $bsBadge->badge();
|
||||
}
|
||||
|
||||
if (!empty($entry['header'])) {
|
||||
return $this->node('h6', [
|
||||
'class' => ['dropdown-header',],
|
||||
], $icon . h($entry['text']) . $badge);
|
||||
}
|
||||
|
||||
$classes = ['dropdown-item'];
|
||||
if (!empty($entry['class'])) {
|
||||
if (!is_array($entry['class'])) {
|
||||
$entry['class'] = [$entry['class']];
|
||||
}
|
||||
$classes = array_merge($classes, $entry['class']);
|
||||
}
|
||||
$params = $entry['attrs'] ?? [];
|
||||
$params['href'] = '#';
|
||||
|
||||
if (!empty($entry['menu'])) {
|
||||
$classes[] = 'dropdown-toggle';
|
||||
$classes[] = 'd-flex align-items-center';
|
||||
$params['data-bs-toggle'] = 'dropdown';
|
||||
$params['aria-haspopup'] = 'true';
|
||||
$params['aria-expanded'] = 'false';
|
||||
if (!empty($entry['keepOpen'])) {
|
||||
$classes[] = 'open-form';
|
||||
}
|
||||
$params['data-open-form-id'] = mt_rand();
|
||||
}
|
||||
|
||||
$labelContent = sprintf(
|
||||
'%s%s',
|
||||
h($entry['text']),
|
||||
!empty($entry['sup']) ? $this->node('sup', ['class' => 'ms-1 text-muted'], $entry['sup']) : ''
|
||||
);
|
||||
$label = $this->node('span', ['class' => 'mx-1'], $labelContent);
|
||||
$content = $icon . $label . $badge;
|
||||
|
||||
return $this->node('a', array_merge([
|
||||
'class' => $classes,
|
||||
], $params), $content);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
|
||||
/**
|
||||
* Creates an icon relying on the FontAwesome library.
|
||||
*
|
||||
* # Options:
|
||||
* - class: Additional classes to add
|
||||
* - title: A title to add to the icon
|
||||
* - attrs: Additional HTML parameters to add
|
||||
*
|
||||
* # Usage:
|
||||
* $this->Bootstrap->icon('eye-slash', [
|
||||
* 'class' => 'm-3',
|
||||
* ]);
|
||||
*/
|
||||
class BootstrapIcon extends BootstrapGeneric
|
||||
{
|
||||
private $icon = '';
|
||||
private $defaultOptions = [
|
||||
'class' => [],
|
||||
'title' => '',
|
||||
'attrs' => [],
|
||||
];
|
||||
|
||||
function __construct(string $icon, array $options = [])
|
||||
{
|
||||
$this->icon = $icon;
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->checkOptionValidity();
|
||||
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||
}
|
||||
|
||||
public function icon(): string
|
||||
{
|
||||
return $this->genIcon();
|
||||
}
|
||||
|
||||
private function genIcon(): string
|
||||
{
|
||||
$html = $this->node('span', array_merge(
|
||||
[
|
||||
'class' => array_merge(
|
||||
$this->options['class'],
|
||||
["fa fa-{$this->icon}"]
|
||||
),
|
||||
'title' => h($this->options['title'])
|
||||
],
|
||||
$this->options['attrs']
|
||||
));
|
||||
return $html;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
|
||||
/**
|
||||
* Creates a Bootstrap list group where items can be links or buttons
|
||||
*
|
||||
* # Options for list container
|
||||
* - class: A list of class
|
||||
* - attrs: A list of additional HTML attributes
|
||||
*
|
||||
* # Options for list items
|
||||
* - href: Link location
|
||||
* - text: Text content of the item
|
||||
* - html: Html content of the item
|
||||
* - class: A list of class
|
||||
* - attrs: A list of additional HTML attributes
|
||||
* - badge: Options to be passed to BootstrapElements\BootstrapBadge
|
||||
*
|
||||
* Usage:
|
||||
* $this->Bootstrap->listGroup(
|
||||
* [
|
||||
* [
|
||||
* 'text' => 'test',
|
||||
* 'badge' => [
|
||||
* 'text' => 'test',
|
||||
* 'variant' => 'warning'
|
||||
* ],
|
||||
* 'attrs' => [
|
||||
* 'data-test' => 'tes'
|
||||
* ]
|
||||
* ],
|
||||
* [
|
||||
* 'html' => '<i>test2</i>',
|
||||
* ],
|
||||
* ],
|
||||
* [
|
||||
* 'class' => 'container-class'
|
||||
* ]
|
||||
* );
|
||||
*/
|
||||
class BootstrapListGroup extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'class' => [],
|
||||
'attrs' => [],
|
||||
];
|
||||
|
||||
private $defaultItemOptions = [
|
||||
'href' => '#',
|
||||
'text' => '',
|
||||
'html' => null,
|
||||
'badge' => '',
|
||||
'class' => [],
|
||||
'attrs' => [],
|
||||
];
|
||||
|
||||
private static $defaultClasses = ['list-group',];
|
||||
private static $defaultItemClasses = ['list-group-item', 'list-group-item-action', 'd-flex', 'align-items-start', 'justify-content-between'];
|
||||
|
||||
function __construct(array $items, array $options, \App\View\BootstrapHelper $btHelper)
|
||||
{
|
||||
$this->items = $items;
|
||||
$this->processOptions($options);
|
||||
$this->btHelper = $btHelper;
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||
}
|
||||
|
||||
public function listGroup()
|
||||
{
|
||||
return $this->genListGroup();
|
||||
}
|
||||
|
||||
private function genListGroup()
|
||||
{
|
||||
$html = $this->nodeOpen('div', array_merge([
|
||||
'class' => array_merge(self::$defaultClasses, $this->options['class']),
|
||||
], $this->options['attrs']));
|
||||
foreach ($this->items as $item) {
|
||||
$html .= $this->genItem($item);
|
||||
}
|
||||
$html .= $this->nodeClose('div');
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genItem(array $item): string
|
||||
{
|
||||
$item['class'] = !is_array($item['class']) ? [$item['class']] : $item['class'];
|
||||
$itemOptions = array_merge($this->defaultItemOptions, $item);
|
||||
$itemOptions['class'] = array_merge(self::$defaultItemClasses, $itemOptions['class']);
|
||||
|
||||
$html = $this->node('a',
|
||||
array_merge([
|
||||
'class' => array_merge(self::$defaultItemClasses, $itemOptions['class']),
|
||||
'href' => '#',
|
||||
], $itemOptions['attrs']),
|
||||
[
|
||||
!is_null($itemOptions['html']) ? $this->node('div', ['class' => 'w-100'], $itemOptions['html']) : h($itemOptions['text']),
|
||||
$this->genBadge($itemOptions['badge'])
|
||||
],
|
||||
);
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genBadge(array $badge): string
|
||||
{
|
||||
if (empty($badge)) {
|
||||
return '';
|
||||
}
|
||||
return $this->btHelper->badge($badge);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use Cake\Utility\Hash;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
use App\View\Helper\BootstrapHelper;
|
||||
|
||||
/**
|
||||
* Creates a list looking like a table from 1-dimensional data $item.
|
||||
* Perfect to display the Key-Values of an object.
|
||||
*
|
||||
* # Options for table
|
||||
* - striped, bordered, borderless, hover, small: Default bootstrap behavior
|
||||
* - variant: Variant to apply on the entire table
|
||||
* - tableClass: A list of class to add on the table container
|
||||
* - bodyClass: A list of class to add on the tbody container
|
||||
* - id: The ID to use for the table
|
||||
* - caption: Optional table caption
|
||||
* - elementsRootPath: Root path to use when item are relying on cakephp's element. See options for fields
|
||||
*
|
||||
* # Items
|
||||
* - They have the content that's used to generate the table. Typically and array<array> or array<entity>
|
||||
*
|
||||
* # Options for fields
|
||||
* - key: The name of the field to be displayed as a label
|
||||
* - keyHtml: The HTML of the field to be displayed as a label
|
||||
* - path: The path to be fed to Hash::get() in order to get the value from the $item
|
||||
* - raw: The raw value to be displayed. Disable the `path` option
|
||||
* - rawNoEscaping: If the raw value should not be escaped. False by default
|
||||
* - type: The type of element to use combined with $elementsRootPath from the table's option
|
||||
* - formatter: A callback function to format the value
|
||||
* - cellVariant: The bootstrap variant to be applied on the cell
|
||||
* - rowVariant: The bootstrap variant to be applied on the row
|
||||
* - notice_$variant: A text with the passed variant to be append at the end
|
||||
*
|
||||
* # Usage:
|
||||
* $this->Bootstrap->listTable(
|
||||
* [
|
||||
* 'hover' => false,
|
||||
* 'variant' => 'success',
|
||||
* ],
|
||||
* [
|
||||
* 'item' => [
|
||||
* 'key1' => 'value1',
|
||||
* 'key2' => true,
|
||||
* 'key3' => 'value3',
|
||||
* ],
|
||||
* 'fields' => [
|
||||
* [
|
||||
* 'key' => 'Label 1',
|
||||
* 'path' => 'key1',
|
||||
* 'notice_warning' => '::warning::',
|
||||
* 'notice_danger' => '::danger::',
|
||||
* 'rowVariant' => 'danger',
|
||||
* 'cellVariant' => 'success',
|
||||
* ],
|
||||
* [
|
||||
* 'key' => 'Label 2',
|
||||
* 'path' => 'key2',
|
||||
* 'type' => 'boolean',
|
||||
* ],
|
||||
* [
|
||||
* 'key' => 'Label 3',
|
||||
* 'raw' => '<b>raw_value</b>',
|
||||
* 'rawNoEscaping' => true,
|
||||
* ],
|
||||
* [
|
||||
* 'key' => 'Label 4',
|
||||
* 'path' => 'key3',
|
||||
* 'formatter' => function ($value) {
|
||||
* return '<i>' . $value . '</i>';
|
||||
* },
|
||||
* ],
|
||||
* ],
|
||||
* 'caption' => 'This is a caption'
|
||||
* ]
|
||||
* );
|
||||
*/
|
||||
class BootstrapListTable extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'striped' => true,
|
||||
'bordered' => false,
|
||||
'borderless' => false,
|
||||
'hover' => true,
|
||||
'small' => false,
|
||||
'variant' => '',
|
||||
'tableClass' => [],
|
||||
'bodyClass' => [],
|
||||
'id' => '',
|
||||
'caption' => '',
|
||||
'elementsRootPath' => '/genericElements/SingleViews/Fields/',
|
||||
];
|
||||
|
||||
function __construct(array $options, array $data, BootstrapHelper $btHelper)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => array_merge(BootstrapGeneric::$variants, [''])
|
||||
];
|
||||
$this->processOptions($options);
|
||||
$this->fields = $data['fields'];
|
||||
$this->item = $data['item'];
|
||||
$this->caption = !empty($data['caption']) ? $data['caption'] : '';
|
||||
$this->btHelper = $btHelper;
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->options['tableClass'] = $this->convertToArrayIfNeeded($this->options['tableClass']);
|
||||
$this->options['bodyClass'] = $this->convertToArrayIfNeeded($this->options['bodyClass']);
|
||||
$this->checkOptionValidity();
|
||||
}
|
||||
|
||||
public function table(): string
|
||||
{
|
||||
return $this->genTable();
|
||||
}
|
||||
|
||||
private function genTable(): string
|
||||
{
|
||||
$html = $this->nodeOpen('table', [
|
||||
'class' => [
|
||||
'table',
|
||||
"table-{$this->options['variant']}",
|
||||
$this->options['striped'] ? 'table-striped' : '',
|
||||
$this->options['bordered'] ? 'table-bordered' : '',
|
||||
$this->options['borderless'] ? 'table-borderless' : '',
|
||||
$this->options['hover'] ? 'table-hover' : '',
|
||||
$this->options['small'] ? 'table-sm' : '',
|
||||
implode(' ', $this->options['tableClass']),
|
||||
!empty($this->options['variant']) ? "table-{$this->options['variant']}" : '',
|
||||
],
|
||||
'id' => $this->options['id'] ?? ''
|
||||
]);
|
||||
|
||||
$html .= $this->genCaption();
|
||||
$html .= $this->genBody();
|
||||
|
||||
$html .= $this->nodeClose('table');
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genBody(): string
|
||||
{
|
||||
$body = $this->nodeOpen('tbody', [
|
||||
'class' => $this->options['bodyClass'],
|
||||
]);
|
||||
foreach ($this->fields as $i => $field) {
|
||||
$body .= $this->genRow($field);
|
||||
}
|
||||
$body .= $this->nodeClose('tbody');
|
||||
return $body;
|
||||
}
|
||||
|
||||
private function genRow(array $field): string
|
||||
{
|
||||
$rowValue = $this->genCell($field);
|
||||
$rowKey = $this->node('th', [
|
||||
'class' => [
|
||||
'col-4 col-sm-2'
|
||||
],
|
||||
'scope' => 'row'
|
||||
], $field['keyHtml'] ?? h($field['key']));
|
||||
$row = $this->node('tr', [
|
||||
'class' => [
|
||||
'd-flex',
|
||||
!empty($field['rowVariant']) ? "table-{$field['rowVariant']}" : ''
|
||||
]
|
||||
], [$rowKey, $rowValue]);
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function genCell(array $field = []): string
|
||||
{
|
||||
if (isset($field['raw'])) {
|
||||
$cellContent = !empty($field['rawNoEscaping']) ? $field['raw'] : h($field['raw']);
|
||||
} else if (isset($field['formatter'])) {
|
||||
$cellContent = $field['formatter']($this->getValueFromObject($field), $this->item);
|
||||
} else if (isset($field['type'])) {
|
||||
$cellContent = $this->btHelper->getView()->element($this->getElementPath($field['type']), [
|
||||
'data' => $this->item,
|
||||
'field' => $field
|
||||
]);
|
||||
} else {
|
||||
$cellContent = h($this->getValueFromObject($field));
|
||||
}
|
||||
foreach (BootstrapGeneric::$variants as $variant) {
|
||||
if (!empty($field["notice_$variant"])) {
|
||||
$cellContent .= sprintf(' <span class="text-%s">%s</span>', $variant, $field["notice_$variant"]);
|
||||
}
|
||||
}
|
||||
return $this->node('td', [
|
||||
'class' => [
|
||||
'col-8 col-sm-10',
|
||||
!empty($field['cellVariant']) ? "bg-{$field['cellVariant']}" : ''
|
||||
]
|
||||
], $cellContent);
|
||||
}
|
||||
|
||||
private function getValueFromObject(array $field): string
|
||||
{
|
||||
$key = is_array($field) ? $field['path'] : $field;
|
||||
$cellValue = Hash::get($this->item, $key);
|
||||
return !is_null($cellValue) ? $cellValue : '';
|
||||
}
|
||||
|
||||
private function getElementPath($type): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s%sField',
|
||||
$this->options['elementsRootPath'] ?? '',
|
||||
$type
|
||||
);
|
||||
}
|
||||
|
||||
private function genCaption(): string
|
||||
{
|
||||
return !empty($this->caption) ? $this->node('caption', [], h($this->caption)) : '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,349 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
|
||||
/**
|
||||
* Creates a bootstrap modal based on the given options
|
||||
*
|
||||
* # Options
|
||||
* - size: Control the horizontal size of the modal. Valid values: 'sm', 'lg', 'xl'
|
||||
* - centered, scrollable, backdropStatic: Default bootstrap behavior
|
||||
* - show: Immediately instantiate the modal and show it
|
||||
* - header-variant, body-variant, footer-variant: Default bootstrap variant to be applied to these modal sections
|
||||
* - title: The title of the modal
|
||||
* - titleHtml: The HTML title of the modal
|
||||
* - body: The body of the modal
|
||||
* - bodyHtml: The HTML body of the modal
|
||||
* - footerHtml: The HTML footer of the modal. Override the $type option
|
||||
* - dialogScrollable: Allows to scroll the modal body
|
||||
* - modalClass, headerClass, footerClass: Classes to be applied to these modal sections
|
||||
* - type: Control the type of actions available.
|
||||
* Valid values: 'ok-only', 'confirm', 'custom'
|
||||
* - The `ok-only` Displays a single 'Ok' button
|
||||
* - The `confirm` Displays a 'Confirm' and 'Cancel' buttons
|
||||
* - `confirmButton` and `cancelButton`: Can be used to pass a BootstrapElements/BootstrapButton configuration
|
||||
* - The `custom` Display a list of button defined in the $footerButtons parameter
|
||||
* - confirmFunction: The function to be called when clicking the "confirm" button
|
||||
* - This options *only* works if the option $show is enabled or if the modal is loaded with the UI ModalFactory function (e.g. `UI.submissionModal()` or `UI.modal()`)
|
||||
* - cancelOnclick: The function to be called once the "cancel" button trigger the `onclick` event
|
||||
* - footerButtons: A list of configuration to be passed to BootstrapElements/BootstrapButton
|
||||
* - The option `clickFunction` can be used to set the function to be called when clicking the button. Behavior similar to "confirmFunction"
|
||||
*
|
||||
* # Click functions behaviors:
|
||||
* - *-Onclick functions have the same behavior as the 'onclick' HTML parameter
|
||||
* - `confirmFunction` and `clickFunction` are called with additional 2 additional arguments:
|
||||
* - modalObject: The instantiated ModalFactory object
|
||||
* - tmpApi: An instantiated AJAXApi object linked with the modal button
|
||||
* - If no functions are provided, Submit the form in place or close the modal
|
||||
*
|
||||
*
|
||||
* # Usage:
|
||||
*
|
||||
* ## Simple styled modal that is displayed automatically when the HTML is attached to the page
|
||||
* $this->Bootstrap->modal([
|
||||
* 'title' => 'Modal title',
|
||||
* 'size' => 'lg',
|
||||
* 'type' => 'ok-only',
|
||||
* 'body' => '<b>Body content</b>',
|
||||
* 'header-variant' => 'dark',
|
||||
* 'body-variant' => 'light',
|
||||
* 'footer-variant' => 'warning',
|
||||
* 'show' => true,
|
||||
* ]);
|
||||
|
||||
* ## Modal with custom onclick handler
|
||||
* $this->Bootstrap->modal([
|
||||
* 'type' => 'confirm',
|
||||
* 'bodyHtml' => '<b>Body content</b>',
|
||||
* 'confirmButton' => [
|
||||
* 'text' => 'Show modal',
|
||||
* 'icon' => 'eye',
|
||||
* 'onclick' => 'UI.toast({"title": "confirmed!"})',
|
||||
* ],
|
||||
* 'cancelOnclick' => 'UI.toast({"title": "cancelled"})',
|
||||
* 'show' => true,
|
||||
* ]);
|
||||
*
|
||||
* ## Modal with a onclick handler with prepared arguments bound to the confirm button
|
||||
* $this->Bootstrap->modal([
|
||||
* 'type' => 'confirm',
|
||||
* 'confirmButton' => [
|
||||
* 'text' => 'Confirm',
|
||||
* 'icon' => 'check',
|
||||
* ],
|
||||
* 'confirmFunction' => 'myConfirmFunction', // myConfirmFunction is called with the $modalObject and $tmpApi intialized
|
||||
* 'show' => true,
|
||||
* ]);
|
||||
*
|
||||
* /*
|
||||
* Example of confirm function
|
||||
* - case 1: If void is returned the modal close automatically regardless of the result
|
||||
* - case 2: If a promise is returned, the modal close automatically if the promise is a success
|
||||
* A success is defined as follow:
|
||||
* - No exceptions
|
||||
* - No data returned
|
||||
* - Object returned with key `success` evaluting to true
|
||||
* - case 3: The modal can be closed manually with: `modalObject.hide()`
|
||||
*
|
||||
* function myConfirmFunction(modalObject, tmpApi) {
|
||||
* const $form = modalObject.$modal.find('form')
|
||||
* const postPromise = $form.length == 1 ?
|
||||
* tmpApi.postForm($form[0]) :
|
||||
* tmpApi.fetchJSON('/users/view/', false, true)
|
||||
* .then((result) => {
|
||||
* console.log(result)
|
||||
* constToReturn = {
|
||||
* success: true, // will close the modal automatically
|
||||
* }
|
||||
* return constToReturn
|
||||
* })
|
||||
* .catch((errors) => {
|
||||
* console.log(errors)
|
||||
* })
|
||||
*
|
||||
* return postPromise
|
||||
* }
|
||||
|
||||
* ## Modal with custom footer made of buttons
|
||||
* $this->Bootstrap->modal([
|
||||
* 'type' => 'custom',
|
||||
* 'footerButtons' => [
|
||||
* [
|
||||
* 'text' => 'Confirm',
|
||||
* 'icon' => 'check',
|
||||
* 'variant' => 'danger',
|
||||
* 'clickFunction' => 'testapi',
|
||||
* ],
|
||||
* [
|
||||
* 'text' => 'Cancel',
|
||||
* 'onclick' => 'UI.toast({"title": "confirmed!"})',
|
||||
* ],
|
||||
* ],
|
||||
* 'show' => true,
|
||||
* ]);
|
||||
*/
|
||||
class BootstrapModal extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'size' => '',
|
||||
'centered' => true,
|
||||
'scrollable' => true,
|
||||
'backdropStatic' => false,
|
||||
'show' => false,
|
||||
'header-variant' => '',
|
||||
'body-variant' => '',
|
||||
'footer-variant' => '',
|
||||
'title' => '',
|
||||
'titleHtml' => null,
|
||||
'body' => '',
|
||||
'bodyHtml' => null,
|
||||
'footerHtml' => null,
|
||||
'dialogScrollable' => true,
|
||||
'modalClass' => [''],
|
||||
'headerClass' => [''],
|
||||
'bodyClass' => [''],
|
||||
'footerClass' => [''],
|
||||
'confirmButton' => [
|
||||
'text' => 'Confirm',
|
||||
],
|
||||
'cancelButton' => [
|
||||
'text' => 'Cancel',
|
||||
],
|
||||
'type' => 'ok-only',
|
||||
'footerButtons' => [],
|
||||
'confirmFunction' => '', // Will be called with the following arguments confirmFunction(modalObject, tmpApi)
|
||||
'cancelOnclick' => ''
|
||||
];
|
||||
|
||||
function __construct(array $options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'size' => ['sm', 'lg', 'xl', ''],
|
||||
'type' => ['ok-only', 'confirm', 'custom'],
|
||||
'header-variant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||
'body-variant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||
'footer-variant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||
];
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->checkOptionValidity();
|
||||
$this->options['modalClass'] = $this->convertToArrayIfNeeded($this->options['modalClass']);
|
||||
$this->options['headerClass'] = $this->convertToArrayIfNeeded($this->options['headerClass']);
|
||||
$this->options['bodyClass'] = $this->convertToArrayIfNeeded($this->options['bodyClass']);
|
||||
$this->options['footerClass'] = $this->convertToArrayIfNeeded($this->options['footerClass']);
|
||||
|
||||
if (!empty($this->options['dialogScrollable'])) {
|
||||
$this->options['modalClass'][] = 'modal-dialog-scrollable';
|
||||
}
|
||||
|
||||
$possiblVariants = ['header-variant', 'body-variant', 'footer-variant'];
|
||||
foreach ($possiblVariants as $possiblVariant) {
|
||||
if (!empty($this->options[$possiblVariant])) {
|
||||
$this->options[sprintf('%sClass', substr($possiblVariant, 0, -8))][] = self::getBGAndTextClassForVariant($this->options[$possiblVariant]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($options['confirmFunction']) && !empty($options['confirmButton']['onclick'])) {
|
||||
throw new \InvalidArgumentException(__('Option `{0}` can not be used in conjuction with `{1}` for the confirm button', 'confirmFunction', 'onclick'));
|
||||
}
|
||||
}
|
||||
|
||||
public function modal(): string
|
||||
{
|
||||
$modal = $this->genModal();
|
||||
if ($this->options['show']) {
|
||||
return $this->encapsulateWithUIHelper($modal);
|
||||
}
|
||||
return $modal;
|
||||
}
|
||||
|
||||
private function encapsulateWithUIHelper(string $modal): string
|
||||
{
|
||||
return $this->node('script', [], sprintf(
|
||||
"$(document).ready(function() {
|
||||
setTimeout(() => {
|
||||
UI.modal({
|
||||
rawHtml: \"%s\"
|
||||
})
|
||||
}, 1);
|
||||
})",
|
||||
str_replace('"', '\"', $modal)
|
||||
));
|
||||
}
|
||||
|
||||
private function genModal(): string
|
||||
{
|
||||
$dialog = $this->nodeOpen('div', [
|
||||
'class' => array_merge(
|
||||
['modal-dialog', (!empty($this->options['size'])) ? "modal-{$this->options['size']}" : ''],
|
||||
$this->options['modalClass']
|
||||
),
|
||||
]);
|
||||
$content = $this->nodeOpen('div', [
|
||||
'class' => ['modal-content'],
|
||||
]);
|
||||
$header = $this->genHeader();
|
||||
$body = $this->genBody();
|
||||
$footer = $this->genFooter();
|
||||
$closedDiv = $this->nodeClose('div');
|
||||
|
||||
$html = "{$dialog}{$content}{$header}{$body}{$footer}{$closedDiv}{$closedDiv}";
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genHeader(): string
|
||||
{
|
||||
$header = $this->nodeOpen('div', ['class' => array_merge(['modal-header'], $this->options['headerClass'])]);
|
||||
$header .= $this->options['titleHtml'] ?? $this->node('h5', ['class' => ['modal-title']], h($this->options['title']));
|
||||
if (empty($this->options['backdropStatic'])) {
|
||||
$header .= $this->genericCloseButton('modal');
|
||||
}
|
||||
$header .= $this->nodeClose('div');
|
||||
return $header;
|
||||
}
|
||||
|
||||
private function genBody(): string
|
||||
{
|
||||
$body = $this->nodeOpen('div', ['class' => array_merge(['modal-body'], $this->options['bodyClass'])]);
|
||||
$body .= $this->options['bodyHtml'] ?? h($this->options['body']);
|
||||
$body .= $this->nodeClose('div');
|
||||
return $body;
|
||||
}
|
||||
|
||||
private function genFooter(): string
|
||||
{
|
||||
$footer = $this->nodeOpen('div', [
|
||||
'class' => array_merge(['modal-footer'], $this->options['footerClass']),
|
||||
'data-custom-footer' => $this->options['type'] == 'custom'
|
||||
]);
|
||||
$footer .= $this->options['footerHtml'] ?? $this->getFooterBasedOnType();
|
||||
$footer .= $this->nodeClose('div');
|
||||
return $footer;
|
||||
}
|
||||
|
||||
private function getFooterBasedOnType(): string
|
||||
{
|
||||
if ($this->options['type'] == 'ok-only') {
|
||||
return $this->getFooterOkOnly();
|
||||
} else if (str_contains($this->options['type'], 'confirm')) {
|
||||
return $this->getFooterConfirm();
|
||||
} else if ($this->options['type'] == 'custom') {
|
||||
return $this->getFooterCustom();
|
||||
} else {
|
||||
return $this->getFooterOkOnly();
|
||||
}
|
||||
}
|
||||
|
||||
private function getFooterOkOnly(): string
|
||||
{
|
||||
return (new BootstrapButton([
|
||||
'variant' => 'primary',
|
||||
'text' => __('Ok'),
|
||||
'onclick' => $this->options['confirmOnclick'],
|
||||
'attrs' => [
|
||||
'data-bs-dismiss' => $this->options['confirmOnclick'] ?? 'modal',
|
||||
],
|
||||
]))->button();
|
||||
}
|
||||
|
||||
private function getFooterConfirm(): string
|
||||
{
|
||||
$buttonCancelConfig = array_merge(
|
||||
[
|
||||
'variant' => 'secondary',
|
||||
'attrs' => [
|
||||
'data-bs-dismiss' => 'modal',
|
||||
'onclick' => $this->options['cancelOnclick']
|
||||
]
|
||||
],
|
||||
$this->options['cancelButton'],
|
||||
);
|
||||
$buttonCancel = (new BootstrapButton($buttonCancelConfig))->button();
|
||||
|
||||
$defaultConfig = [
|
||||
'variant' => 'primary',
|
||||
'class' => 'modal-confirm-button',
|
||||
];
|
||||
if (!empty($this->options['confirmOnclick'])) {
|
||||
$defaultConfig['onclick'] = $this->options['confirmOnclick'];
|
||||
}
|
||||
if (!empty($this->options['confirmFunction'])) {
|
||||
$defaultConfig['attrs']['data-confirmFunction'] = $this->options['confirmFunction'];
|
||||
}
|
||||
$buttonConfirmConfig = array_merge(
|
||||
$defaultConfig,
|
||||
$this->options['confirmButton'],
|
||||
);
|
||||
$buttonConfirm = (new BootstrapButton($buttonConfirmConfig))->button();
|
||||
return $buttonCancel . $buttonConfirm;
|
||||
}
|
||||
|
||||
private function getFooterCustom(): string
|
||||
{
|
||||
$buttons = [];
|
||||
foreach ($this->options['footerButtons'] as $buttonConfig) {
|
||||
$defaultConfig = [
|
||||
'variant' => 'primary',
|
||||
'class' => 'modal-confirm-button',
|
||||
'attrs' => [
|
||||
'data-bs-dismiss' => !empty($buttonConfig['clickFunction']) ? '' : 'modal',
|
||||
]
|
||||
];
|
||||
if (!empty($buttonConfig['clickFunction'])) {
|
||||
$defaultConfig['attrs']['data-clickFunction'] = $buttonConfig['clickFunction'];
|
||||
}
|
||||
$buttonConfig = array_merge(
|
||||
$defaultConfig,
|
||||
$buttonConfig,
|
||||
);
|
||||
$buttons[] = (new BootstrapButton($buttonConfig))->button();
|
||||
}
|
||||
return implode('', $buttons);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
|
||||
/**
|
||||
* Creates a small colored circle meant to show notifications
|
||||
*
|
||||
* # Options
|
||||
* - text: Optinal text to be displayed inside the circle
|
||||
* - variant: The Bootstrap variant of the notification circle
|
||||
* - borderVariant: If set, creates a border around the circle. Typically will hold the value `light` or `dark`
|
||||
* - title: The HTML title of the notification
|
||||
* - class: Additional classes to be added
|
||||
* - attrs: Additional attributes to be added
|
||||
*
|
||||
* # Usage
|
||||
* $this->Bootstrap->notificationBubble([
|
||||
* 'text' => '3',
|
||||
* 'variant' => 'warning',
|
||||
* 'title' => '3 unread messages',
|
||||
* ]);
|
||||
*/
|
||||
class BootstrapNotificationBubble extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'text' => '',
|
||||
'variant' => 'warning',
|
||||
'borderVariant' => '',
|
||||
'title' => '',
|
||||
'class' => [],
|
||||
'attrs' => [],
|
||||
];
|
||||
|
||||
function __construct(array $options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => BootstrapGeneric::$variants,
|
||||
'borderVariant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||
];
|
||||
$this->defaultOptions['title'] = __('New notifications');
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->checkOptionValidity();
|
||||
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||
if (!empty($this->options['borderVariant'])) {
|
||||
if (!empty($this->options['attrs']['style'])) {
|
||||
$this->options['attrs']['style'] .= 'box-shadow: 0 0.125rem 0.25rem #00000050;';
|
||||
} else {
|
||||
$this->options['attrs']['style'] = 'box-shadow: 0 0.125rem 0.25rem #00000050;';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function notificationBubble(): string
|
||||
{
|
||||
return $this->genNotificationBubble();
|
||||
}
|
||||
|
||||
private function genNotificationBubble(): string
|
||||
{
|
||||
$tmpId = 'tmp-' . mt_rand();
|
||||
$defaultClasses = [
|
||||
'position-absolute',
|
||||
'top-0',
|
||||
'start-100',
|
||||
'translate-middle',
|
||||
'p-1',
|
||||
'rounded-circle',
|
||||
];
|
||||
if (!empty($this->options['borderVariant'])) {
|
||||
$defaultClasses[] = "border border-2 border-{$this->options['borderVariant']}";
|
||||
}
|
||||
if (!empty($this->options['variant'])) {
|
||||
$defaultClasses[] = "bg-{$this->options['variant']}";
|
||||
}
|
||||
|
||||
if (!empty($this->options['text'])) {
|
||||
$this->options['attrs']['style'] .= ' min-width: 0.7rem; line-height: 1; box-sizing: content-box;';
|
||||
$defaultClasses[] = 'text-center';
|
||||
$defaultClasses[] = 'fs-8';
|
||||
$defaultClasses[] = 'fw-bold';
|
||||
}
|
||||
|
||||
$html = $this->node('span',
|
||||
array_merge(
|
||||
[
|
||||
'id' => $tmpId,
|
||||
'class' => array_merge(
|
||||
$defaultClasses,
|
||||
$this->options['class']
|
||||
),
|
||||
'title' => h($this->options['title'])
|
||||
],
|
||||
$this->options['attrs']
|
||||
),
|
||||
!empty($this->options['text']) ? $this->node('span', [], h($this->options['text'])) : ''
|
||||
);
|
||||
return $html;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
|
||||
/**
|
||||
* Creates a bootstrap progress bar
|
||||
*
|
||||
* # Options:
|
||||
* - label: A text to be centered in the active part of the progress bar. If set to `true`, will display the percentage of the progress bar
|
||||
* - title: The title HTML attribute to set
|
||||
* - total: The total amount of the progress
|
||||
* - value: The active part of the progress
|
||||
* - variant: The bootstrap variant of the active part of the progress bar
|
||||
* - height: The height of the bar
|
||||
* - striped, animated: If the bar should have the striped and animated bootstrap properties
|
||||
* - attrs: Additional HTML attributes to add
|
||||
*
|
||||
* # Usage:
|
||||
* $this->Bootstrap->progress([
|
||||
* 'value' => 45,
|
||||
* 'total' => 100,
|
||||
* 'label' => true,
|
||||
* ]);
|
||||
*
|
||||
*/
|
||||
class BootstrapProgress extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'value' => 0,
|
||||
'total' => 100,
|
||||
'label' => true,
|
||||
'title' => '',
|
||||
'variant' => 'primary',
|
||||
'height' => '',
|
||||
'striped' => false,
|
||||
'animated' => false,
|
||||
'attrs' => [],
|
||||
];
|
||||
|
||||
function __construct($options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => BootstrapGeneric::$variants,
|
||||
];
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
private function processOptions($options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->checkOptionValidity();
|
||||
}
|
||||
|
||||
public function progress(): string
|
||||
{
|
||||
return $this->genProgress();
|
||||
}
|
||||
|
||||
private function genProgress(): string
|
||||
{
|
||||
$percentage = round(100 * $this->options['value'] / $this->options['total']);
|
||||
$heightStyle = !empty($this->options['height']) ? sprintf('height: %s;', h($this->options['height'])) : '';
|
||||
$widthStyle = sprintf('width: %s%%;', $percentage);
|
||||
$label = !empty($this->options['label']) ? ($this->options['label'] === true ? "{$percentage}%" : h($this->options['label'])) : '';
|
||||
$pb = $this->node('div', array_merge([
|
||||
'class' => [
|
||||
'progress-bar',
|
||||
"bg-{$this->options['variant']}",
|
||||
$this->options['striped'] ? 'progress-bar-striped' : '',
|
||||
$this->options['animated'] ? 'progress-bar-animated' : '',
|
||||
],
|
||||
'role' => "progressbar",
|
||||
'aria-valuemin' => "0", 'aria-valuemax' => "100", 'aria-valuenow' => $percentage,
|
||||
'style' => $widthStyle,
|
||||
'title' => h($this->options['title']),
|
||||
], $this->options['attrs']), $label);
|
||||
$container = $this->node('div', [
|
||||
'class' => [
|
||||
'progress',
|
||||
],
|
||||
'style' => $heightStyle,
|
||||
'title' => h($this->options['title']),
|
||||
], $pb);
|
||||
return $container;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
|
||||
/**
|
||||
* Creates a progress timeline similar to a form wizard
|
||||
*
|
||||
* # Options:
|
||||
* - variant: The variant of the active part of the timeline
|
||||
* - variantInactive: The variant of the inactive part of the timeline
|
||||
* - selected: 0-indexed step number to be selected. Will make all steps before the selected step active
|
||||
* - steps: The definition of the step. Options are:
|
||||
* - text: The text of the step
|
||||
* - icon: The icon of the step. Default to the text number if empty
|
||||
* - title: A title to be set for the step
|
||||
*
|
||||
* # Usage:
|
||||
* $this->Bootstrap->progressTimeline([
|
||||
* 'selected' => 1,
|
||||
* 'steps' => [
|
||||
* [
|
||||
* 'text' => __('Step 1'),
|
||||
* 'icon' => 'star',
|
||||
* 'title' => __('Title'),
|
||||
* ],
|
||||
* [
|
||||
* 'text' => __('Step 3'),
|
||||
* 'icon' => 'exchange-alt',
|
||||
* ]
|
||||
* ],
|
||||
* ]);
|
||||
*/
|
||||
class BootstrapProgressTimeline extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'steps' => [],
|
||||
'selected' => 0,
|
||||
'variant' => 'primary',
|
||||
'variantInactive' => 'secondary',
|
||||
];
|
||||
|
||||
function __construct($options, $btHelper)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => BootstrapGeneric::$variants,
|
||||
'variantInactive' => BootstrapGeneric::$variants,
|
||||
];
|
||||
$this->processOptions($options);
|
||||
$this->btHelper = $btHelper;
|
||||
}
|
||||
|
||||
private function processOptions($options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->checkOptionValidity();
|
||||
}
|
||||
|
||||
public function progressTimeline(): string
|
||||
{
|
||||
return $this->genProgressTimeline();
|
||||
}
|
||||
|
||||
private function getStepIcon(array $step, int $i, bool $nodeActive, bool $lineActive): string
|
||||
{
|
||||
$icon = $this->node('b', [
|
||||
'class' => [
|
||||
!empty($step['icon']) ? h($this->btHelper->FontAwesome->getClass($step['icon'])) : '',
|
||||
$this->getTextClassForVariant($this->options['variant'])
|
||||
],
|
||||
], empty($step['icon']) ? h($i + 1) : '');
|
||||
|
||||
$containerDefaultClass = [
|
||||
'd-flex',
|
||||
'align-items-center',
|
||||
'justify-content-center',
|
||||
'rounded-circle',
|
||||
];
|
||||
$containerDefaultClass[] = $nodeActive ? "bg-{$this->options['variant']}" : "bg-{$this->options['variantInactive']}";
|
||||
$iconContainer = $this->node('span', [
|
||||
'class' => $containerDefaultClass,
|
||||
'style' => 'width:50px; height:50px'
|
||||
], $icon);
|
||||
$li = $this->node('li', [
|
||||
'class' => [
|
||||
'd-flex', 'flex-column',
|
||||
$nodeActive ? 'progress-active' : 'progress-inactive',
|
||||
],
|
||||
], $iconContainer);
|
||||
$html = $li . $this->getHorizontalLine($i, $nodeActive, $lineActive);
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function getHorizontalLine(int $i, bool $nodeActive, bool $lineActive): string
|
||||
{
|
||||
$stepCount = count($this->options['steps']);
|
||||
if ($i == $stepCount - 1) {
|
||||
return '';
|
||||
}
|
||||
$progressBar = (new BootstrapProgress([
|
||||
'label' => false,
|
||||
'value' => $nodeActive ? ($lineActive ? 100 : 50) : 0,
|
||||
'height' => '2px',
|
||||
'variant' => $this->options['variant']
|
||||
]))->progress();
|
||||
$line = $this->node('span', [
|
||||
'class' => [
|
||||
'progress-line',
|
||||
'flex-grow-1', 'align-self-center',
|
||||
$lineActive ? "bg-{$this->options['variant']}" : ''
|
||||
],
|
||||
], $progressBar);
|
||||
return $line;
|
||||
}
|
||||
|
||||
private function getStepText(array $step, bool $isActive): string
|
||||
{
|
||||
return $this->node('li', [
|
||||
'class' => [
|
||||
'text-center',
|
||||
'fw-bold',
|
||||
$isActive ? 'progress-active' : 'progress-inactive',
|
||||
],
|
||||
], h($step['text'] ?? ''));
|
||||
}
|
||||
|
||||
private function genProgressTimeline(): string
|
||||
{
|
||||
$iconLis = '';
|
||||
$textLis = '';
|
||||
foreach ($this->options['steps'] as $i => $step) {
|
||||
$nodeActive = $i <= $this->options['selected'];
|
||||
$lineActive = $i < $this->options['selected'];
|
||||
$iconLis .= $this->getStepIcon($step, $i, $nodeActive, $lineActive);
|
||||
$textLis .= $this->getStepText($step, $nodeActive);
|
||||
}
|
||||
$ulIcons = $this->node('ul', [
|
||||
'class' => [
|
||||
'd-flex', 'justify-content-around',
|
||||
],
|
||||
], $iconLis);
|
||||
$ulText = $this->node('ul', [
|
||||
'class' => [
|
||||
'd-flex', 'justify-content-between',
|
||||
],
|
||||
], $textLis);
|
||||
$html = $this->node('div', [
|
||||
'class' => ['progress-timeline', 'mw-75', 'mx-auto']
|
||||
], $ulIcons . $ulText);
|
||||
return $html;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
|
||||
/**
|
||||
* Creates a switch acting as a checkbox
|
||||
*
|
||||
* # Options:
|
||||
* - label: The label associated with the switch
|
||||
* - disabled: Should the switch be disabled
|
||||
* - checked: Should the switch be checked by default
|
||||
* - title: Optional title to add to the switch
|
||||
* - variant: The variant to use to show if the switch is active
|
||||
* - class: Additional class to add to the switch
|
||||
* - attrs: Additional HTML attributes to add to the switch
|
||||
*
|
||||
* # Usage:
|
||||
* $this->Bootstrap->switch([
|
||||
* 'label' => 'my label',
|
||||
* 'checked' => true,
|
||||
* ]);
|
||||
*/
|
||||
class BootstrapSwitch extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'label' => '',
|
||||
'variant' => 'primary',
|
||||
'disabled' => false,
|
||||
'checked' => false,
|
||||
'title' => '',
|
||||
'class' => [],
|
||||
'attrs' => [],
|
||||
];
|
||||
|
||||
public function __construct(array $options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => BootstrapGeneric::$variants,
|
||||
];
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->checkOptionValidity();
|
||||
}
|
||||
|
||||
public function switch(): string
|
||||
{
|
||||
return $this->genSwitch();
|
||||
}
|
||||
|
||||
public function genSwitch(): string
|
||||
{
|
||||
$tmpId = 'tmp-' . mt_rand();
|
||||
$input = self::node('input', array_merge(
|
||||
[
|
||||
'type' => "checkbox",
|
||||
'class' => 'form-check-input',
|
||||
'id' => $tmpId,
|
||||
'disabled' => !empty($this->options['disabled']),
|
||||
'checked' => !empty($this->options['checked']),
|
||||
],
|
||||
$this->options['attrs']
|
||||
));
|
||||
$label = self::node('label', [
|
||||
'class' => 'form-check-label',
|
||||
'for' => $tmpId,
|
||||
], h($this->options['label']));
|
||||
$html = self::node('div', [
|
||||
'class' => [
|
||||
'form-check form-switch',
|
||||
],
|
||||
'title' => h($this->options['title']),
|
||||
], [$input, $label]);
|
||||
return $html;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\Utility\Inflector;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
use App\View\Helper\BootstrapHelper;
|
||||
|
||||
/**
|
||||
* Creates a table from 2-dimensional data $items.
|
||||
* Perfect to display a list of objects.
|
||||
*
|
||||
* # Options for table
|
||||
* - striped, bordered, borderless, hover, small: Default bootstrap behavior
|
||||
* - variant: Variant to apply on the entire table
|
||||
* - tableClass: A list of class to add on the table container
|
||||
* - bodyClass: A list of class to add on the tbody container
|
||||
* - id: The ID to use for the table
|
||||
* - caption: Optional table caption
|
||||
* - elementsRootPath: Root path to use when item are relying on cakephp's element. See options for fields
|
||||
*
|
||||
* # Options for fields
|
||||
* - label: The name of the field to be displayed as a label
|
||||
* - labelHtml: The HTML of the field to be displayed as a label
|
||||
* - class: Additional classes to add for that row
|
||||
* - path: The path to be fed to Hash::get() in order to get the value from the $item
|
||||
* - element: The type of element to use combined with $elementsRootPath from the table's option
|
||||
* - formatter: A callback function to format the value
|
||||
* - columnVariant: The bootstrap variant to be applied on the cell
|
||||
* - notice_$variant: A text with the passed variant to be append at the end. $variant can be any valid bootstrap variant. Example: `notice_warning` or `notice_info`.
|
||||
*
|
||||
* # Special fields for $items
|
||||
* - _rowVariant: The bootstrap variant to be applied on the row
|
||||
*
|
||||
* # Usage:
|
||||
* $this->Bootstrap->table(
|
||||
* [
|
||||
* 'hover' => false,
|
||||
* 'striped' => false,
|
||||
* ],
|
||||
* [
|
||||
* 'items' => [
|
||||
* ['column 1' => 'col1', 'column 2' => 'col2', 'key1' => 'val1', 'key2' => true],
|
||||
* ['column 1' => 'col1', 'column 2' => 'col2', 'key1' => 'val2', 'key2' => false,'_rowVariant' => 'success'],
|
||||
* ['column 1' => 'col1', 'column 2' => 'col2', 'key1' => 'val3', 'key2' => true],
|
||||
* ],
|
||||
* 'fields' => [
|
||||
* 'column 1',
|
||||
* [
|
||||
* 'path' => 'column 2',
|
||||
* 'label' => 'COLUMN 2',
|
||||
* 'columnVariant' => 'danger',
|
||||
* ],
|
||||
* [
|
||||
* 'labelHtml' => '<i>column 3</i>',
|
||||
* ],
|
||||
* [
|
||||
* 'path' => 'key1',
|
||||
* 'label' => __('Field'),
|
||||
* 'formatter' => function ($field, $row) {
|
||||
* return sprintf('<i>%s</i>', h($field));
|
||||
* }
|
||||
* ],
|
||||
* [
|
||||
* 'path' => 'key2',
|
||||
* 'element' => 'boolean',
|
||||
* ],
|
||||
* ],
|
||||
* 'caption' => 'This is a caption'
|
||||
* ]
|
||||
* );
|
||||
*/
|
||||
class BootstrapTable extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'striped' => true,
|
||||
'bordered' => true,
|
||||
'borderless' => false,
|
||||
'hover' => true,
|
||||
'small' => false,
|
||||
'variant' => '',
|
||||
'tableClass' => [],
|
||||
'headerClass' => [],
|
||||
'bodyClass' => [],
|
||||
'id' => '',
|
||||
'caption' => '',
|
||||
'elementsRootPath' => '/genericElements/SingleViews/Fields/',
|
||||
];
|
||||
|
||||
function __construct(array $options, array $data, BootstrapHelper $btHelper)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => array_merge(BootstrapGeneric::$variants, [''])
|
||||
];
|
||||
$this->processOptions($options);
|
||||
$this->fields = $data['fields'];
|
||||
$this->items = $data['items'];
|
||||
$this->caption = !empty($data['caption']) ? $data['caption'] : '';
|
||||
$this->btHelper = $btHelper;
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->checkOptionValidity();
|
||||
$this->options['tableClass'] = $this->convertToArrayIfNeeded($this->options['tableClass']);
|
||||
$this->options['bodyClass'] = $this->convertToArrayIfNeeded($this->options['bodyClass']);
|
||||
$this->options['headerClass'] = $this->convertToArrayIfNeeded($this->options['headerClass']);
|
||||
}
|
||||
|
||||
public function table(): string
|
||||
{
|
||||
return $this->genTable();
|
||||
}
|
||||
|
||||
private function genTable(): string
|
||||
{
|
||||
$html = $this->nodeOpen('table', [
|
||||
'class' => [
|
||||
'table',
|
||||
"table-{$this->options['variant']}",
|
||||
$this->options['striped'] ? 'table-striped' : '',
|
||||
$this->options['bordered'] ? 'table-bordered' : '',
|
||||
$this->options['borderless'] ? 'table-borderless' : '',
|
||||
$this->options['hover'] ? 'table-hover' : '',
|
||||
$this->options['small'] ? 'table-sm' : '',
|
||||
implode(' ', $this->options['tableClass']),
|
||||
!empty($this->options['variant']) ? "table-{$this->options['variant']}" : '',
|
||||
],
|
||||
'id' => $this->options['id'] ?? ''
|
||||
]);
|
||||
|
||||
$html .= $this->genCaption();
|
||||
$html .= $this->genHeader();
|
||||
$html .= $this->genBody();
|
||||
|
||||
$html .= $this->nodeClose('table');
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genHeader(): string
|
||||
{
|
||||
$head = $this->nodeOpen('thead', [
|
||||
'class' => $this->options['headerClass'],
|
||||
]);
|
||||
$head .= $this->nodeOpen('tr');
|
||||
foreach ($this->fields as $i => $field) {
|
||||
if (is_array($field)) {
|
||||
if (!empty($field['labelHtml'])) {
|
||||
$label = $field['labelHtml'];
|
||||
} else {
|
||||
$label = !empty($field['label']) ? $field['label'] : Inflector::humanize($field['path']);
|
||||
$label = h($label);
|
||||
}
|
||||
} else {
|
||||
$label = Inflector::humanize($field);
|
||||
$label = h($label);
|
||||
}
|
||||
$head .= $this->node('th', [], $label);
|
||||
}
|
||||
$head .= $this->nodeClose('tr');
|
||||
$head .= $this->nodeClose('thead');
|
||||
return $head;
|
||||
}
|
||||
|
||||
private function genBody(): string
|
||||
{
|
||||
$body = $this->nodeOpen('tbody', [
|
||||
'class' => $this->options['bodyClass'],
|
||||
]);
|
||||
foreach ($this->items as $i => $row) {
|
||||
$body .= $this->genRow($row, $i);
|
||||
}
|
||||
$body .= $this->nodeClose('tbody');
|
||||
return $body;
|
||||
}
|
||||
|
||||
private function genRow(array $row, int $rowIndex): string
|
||||
{
|
||||
$html = $this->nodeOpen('tr', [
|
||||
'class' => [
|
||||
!empty($row['_rowVariant']) ? "table-{$row['_rowVariant']}" : ''
|
||||
]
|
||||
]);
|
||||
if (array_keys($row) !== range(0, count($row) - 1)) { // associative array
|
||||
foreach ($this->fields as $i => $field) {
|
||||
$cellValue = $this->getValueFromObject($row, $field);
|
||||
$html .= $this->genCell($cellValue, $field, $row, $rowIndex);
|
||||
}
|
||||
} else { // indexed array
|
||||
foreach ($row as $i => $cellValue) {
|
||||
$html .= $this->genCell($cellValue, $this->fields[$i], $row, $rowIndex);
|
||||
}
|
||||
}
|
||||
$html .= $this->nodeClose('tr');
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genCell($value, array $field = [], array $row = [], int $rowIndex = 0): string
|
||||
{
|
||||
if (isset($field['formatter'])) {
|
||||
$cellContent = $field['formatter']($value, $row, $rowIndex);
|
||||
} else if (isset($field['element'])) {
|
||||
$cellContent = $this->btHelper->getView()->element($this->getElementPath($field['element']), [
|
||||
'data' => [$value],
|
||||
'field' => ['path' => '0']
|
||||
]);
|
||||
} else {
|
||||
$cellContent = h($value);
|
||||
}
|
||||
return $this->node('td', [
|
||||
'class' => array_merge(
|
||||
[
|
||||
!empty($field['columnVariant']) ? "table-{$field['columnVariant']}" : ''
|
||||
],
|
||||
$field['class'] ?? []
|
||||
),
|
||||
], $cellContent);
|
||||
}
|
||||
|
||||
private function getValueFromObject(array $row, $field)
|
||||
{
|
||||
$path = is_array($field) ? $field['path'] : $field;
|
||||
$cellValue = Hash::get($row, $path);
|
||||
return !is_null($cellValue) ? $cellValue : '';
|
||||
}
|
||||
|
||||
private function getElementPath(string $type): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s%sField',
|
||||
$this->options['elementsRootPath'] ?? '',
|
||||
$type
|
||||
);
|
||||
}
|
||||
|
||||
private function genCaption(): string
|
||||
{
|
||||
return !empty($this->caption) ? $this->node('caption', [], h($this->caption)) : '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,307 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use Cake\Utility\Security;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
|
||||
/**
|
||||
* Creates a bootstrap panel with navigation component.
|
||||
*
|
||||
* # Options:
|
||||
* - fill-header: Should the navigation header takes up all the space available
|
||||
* - justify-header: Allow to specify how the naviation component should be justified. Accepts: false (no justify), 'start', 'end', 'center';
|
||||
* - pills: Should the navigation element be pills
|
||||
* - card: Should the content and navigation elements be wrapped in a Bootstrap card component
|
||||
* - header-variant, body-variant: The variant that the card's header and body should have. Ignore if $card is not set
|
||||
* - body-class, nav-class, nav-class-item, content-class: Additional classes to be added to the nav, body, navigation items or content
|
||||
* - vertical: Should the navigation component be placed vertically next to the content. Best used with the `pills` option enabled.
|
||||
* - vertical-size: Controls the horizontal size of the vertical header. Must be between [1, 11]
|
||||
* - vertical-position: Controls the position of the header. Accepts 'start and 'end'
|
||||
* - horizontal-position: Controls the position of the header. Accepts 'top and 'bottom'
|
||||
* - data: The data used to generate the tabs. Must have a `navs` and `content` key. See the "# Data" section
|
||||
*
|
||||
* # Data
|
||||
* - navs: The data for the navigation items. Supported options:
|
||||
* - id: The ID of the nav. Auto-generated if left empty
|
||||
* - active: Should the tab be active
|
||||
* - disabled: Should the tab be disabled
|
||||
* - text: The text content of the tab
|
||||
* - html: The HTML content of the tab
|
||||
*
|
||||
* - content: The HTML content for each tabs
|
||||
*
|
||||
* # Usage:
|
||||
* ## Simple formatted tabs using the card option
|
||||
* echo $this->Bootstrap->tabs([
|
||||
* 'horizontal-position' => 'top',
|
||||
* 'header-variant' => 'danger',
|
||||
* 'card' => true,
|
||||
* 'data' => [
|
||||
* 'navs' => [
|
||||
* ['text' => 'nav 1'],
|
||||
* ['html' => '<b>nav 2</b>', 'active' => true],
|
||||
* ],
|
||||
* 'content' => [
|
||||
* '<i>content 1</i>',
|
||||
* 'content 2',
|
||||
* ]
|
||||
* ]
|
||||
* ]);
|
||||
*
|
||||
* ## Simple formatted tabs using the card option and vertical options
|
||||
* echo $this->Bootstrap->tabs([
|
||||
* 'pills' => true,
|
||||
* 'vertical' => true,
|
||||
* 'vertical-position' => 'start',
|
||||
* 'card' => true,
|
||||
* 'data' => [
|
||||
* 'navs' => [
|
||||
* ['text' => 'nav 1'],
|
||||
* ['html' => '<b>nav 2</b>', 'disabled' => true],
|
||||
* ],
|
||||
* 'content' => [
|
||||
* '<i>content 1</i>',
|
||||
* 'content 2',
|
||||
* ]
|
||||
* ]
|
||||
* ]);
|
||||
*/
|
||||
class BootstrapTabs extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'fill-header' => false,
|
||||
'justify-header' => false,
|
||||
'pills' => false,
|
||||
'vertical' => false,
|
||||
'vertical-size' => 3,
|
||||
'vertical-position' => 'start',
|
||||
'horizontal-position' => 'top',
|
||||
'card' => false,
|
||||
'header-variant' => '',
|
||||
'body-variant' => '',
|
||||
'body-class' => [],
|
||||
'nav-class' => [],
|
||||
'nav-item-class' => [],
|
||||
'content-class' => [],
|
||||
'data' => [
|
||||
'navs' => [],
|
||||
'content' => [],
|
||||
],
|
||||
];
|
||||
private $bsClasses = null;
|
||||
|
||||
function __construct(array $options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'justify-header' => [false, 'center', 'end', 'start'],
|
||||
'vertical-position' => ['start', 'end'],
|
||||
'horizontal-position' => ['top', 'bottom'],
|
||||
'body-variant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||
'header-variant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||
];
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
public function tabs(): string
|
||||
{
|
||||
return $this->genTabs();
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->data = $this->options['data'];
|
||||
$this->checkOptionValidity();
|
||||
if (empty($this->data['navs'])) {
|
||||
throw new InvalidArgumentException(__('No navigation data provided'));
|
||||
}
|
||||
$this->bsClasses = [
|
||||
'nav' => [],
|
||||
'nav-item' => $this->options['nav-item-class'],
|
||||
];
|
||||
|
||||
if (!empty($this->options['justify-header'])) {
|
||||
$this->bsClasses['nav'][] = 'justify-content-' . $this->options['justify-header'];
|
||||
}
|
||||
|
||||
if ($this->options['vertical'] && !isset($options['pills']) && !isset($options['card'])) {
|
||||
$this->options['pills'] = true;
|
||||
$this->options['card'] = true;
|
||||
}
|
||||
|
||||
if ($this->options['pills']) {
|
||||
$this->bsClasses['nav'][] = 'nav-pills';
|
||||
if ($this->options['vertical']) {
|
||||
$this->bsClasses['nav'][] = 'flex-column';
|
||||
}
|
||||
if ($this->options['card']) {
|
||||
$this->bsClasses['nav'][] = 'card-header-pills';
|
||||
}
|
||||
} else {
|
||||
$this->bsClasses['nav'][] = 'nav-tabs';
|
||||
if ($this->options['card']) {
|
||||
$this->bsClasses['nav'][] = 'card-header-tabs';
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->options['fill-header']) {
|
||||
$this->bsClasses['nav'][] = 'nav-fill';
|
||||
}
|
||||
if ($this->options['justify-header']) {
|
||||
$this->bsClasses['nav'][] = 'nav-justify';
|
||||
}
|
||||
|
||||
$activeTab = array_key_first($this->data['navs']);
|
||||
foreach ($this->data['navs'] as $i => $nav) {
|
||||
if (!is_array($nav)) {
|
||||
$this->data['navs'][$i] = ['text' => $nav];
|
||||
}
|
||||
if (!isset($this->data['navs'][$i]['id'])) {
|
||||
$this->data['navs'][$i]['id'] = 't-' . Security::randomString(8);
|
||||
}
|
||||
if (!empty($nav['active'])) {
|
||||
$activeTab = $i;
|
||||
}
|
||||
}
|
||||
$this->data['navs'][$activeTab]['active'] = true;
|
||||
|
||||
if (!empty($this->options['vertical-size']) && $this->options['vertical-size'] != 'auto') {
|
||||
$this->options['vertical-size'] = ($this->options['vertical-size'] < 0 || $this->options['vertical-size'] > 11) ? 3 : $this->options['vertical-size'];
|
||||
}
|
||||
|
||||
if (!is_array($this->options['nav-class'])) {
|
||||
$this->options['nav-class'] = [$this->options['nav-class']];
|
||||
}
|
||||
if (!is_array($this->options['content-class'])) {
|
||||
$this->options['content-class'] = [$this->options['content-class']];
|
||||
}
|
||||
}
|
||||
|
||||
private function genTabs(): string
|
||||
{
|
||||
return $this->options['vertical'] ? $this->genVerticalTabs() : $this->genHorizontalTabs();
|
||||
}
|
||||
|
||||
private function genHorizontalTabs(): string
|
||||
{
|
||||
if ($this->options['card']) {
|
||||
$cardOptions = [
|
||||
'bodyHTML' => $this->genContent(),
|
||||
'bodyVariant' => $this->options['body-variant'],
|
||||
];
|
||||
if ($this->options['horizontal-position'] === 'bottom') {
|
||||
$cardOptions['footerHTML'] = $this->genNav();
|
||||
$cardOptions['footerVariant'] = $this->options['header-variant'];
|
||||
$cardOptions['headerVariant'] = $this->options['header-variant'];
|
||||
} else {
|
||||
$cardOptions['headerHTML'] = $this->genNav();
|
||||
$cardOptions['headerVariant'] = $this->options['header-variant'];
|
||||
}
|
||||
$bsCard = new BootstrapCard($cardOptions);
|
||||
return $bsCard->card();
|
||||
} else {
|
||||
return $this->genNav() . $this->genContent();
|
||||
}
|
||||
}
|
||||
|
||||
private function genVerticalTabs(): string
|
||||
{
|
||||
$header = $this->node('div', ['class' => array_merge(
|
||||
[
|
||||
($this->options['vertical-size'] != 'auto' ? 'col-' . $this->options['vertical-size'] : ''),
|
||||
($this->options['card'] ? 'card-header border-end' : '')
|
||||
],
|
||||
[
|
||||
"bg-{$this->options['header-variant']}",
|
||||
"text-{$this->options['header-text-variant']}",
|
||||
"border-{$this->options['header-border-variant']}"
|
||||
]
|
||||
)], $this->genNav());
|
||||
$content = $this->node('div', ['class' => array_merge(
|
||||
[
|
||||
($this->options['vertical-size'] != 'auto' ? 'col-' . (12 - $this->options['vertical-size']) : ''),
|
||||
($this->options['card'] ? 'card-body2' : '')
|
||||
],
|
||||
[
|
||||
"bg-{$this->options['body-variant']}",
|
||||
"text-{$this->options['body-text-variant']}"
|
||||
]
|
||||
)], $this->genContent());
|
||||
|
||||
$containerContent = $this->options['vertical-position'] === 'start' ? [$header, $content] : [$content, $header];
|
||||
$container = $this->node('div', ['class' => array_merge(
|
||||
[
|
||||
'row',
|
||||
($this->options['card'] ? 'card flex-row' : ''),
|
||||
($this->options['vertical-size'] == 'auto' ? 'flex-nowrap' : '')
|
||||
],
|
||||
[
|
||||
"border-{$this->options['header-border-variant']}"
|
||||
]
|
||||
)], $containerContent);
|
||||
return $container;
|
||||
}
|
||||
|
||||
private function genNav(): string
|
||||
{
|
||||
$html = $this->nodeOpen('ul', [
|
||||
'class' => array_merge(['nav'], $this->bsClasses['nav'], $this->options['nav-class']),
|
||||
'role' => 'tablist',
|
||||
]);
|
||||
foreach ($this->data['navs'] as $navItem) {
|
||||
$html .= $this->genNavItem($navItem);
|
||||
}
|
||||
$html .= $this->nodeClose('ul');
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genNavItem(array $navItem): string
|
||||
{
|
||||
$html = $this->nodeOpen('li', [
|
||||
'class' => array_merge(['nav-item'], $this->bsClasses['nav-item'], $this->options['nav-item-class']),
|
||||
'role' => 'presentation',
|
||||
]);
|
||||
$html .= $this->nodeOpen('a', [
|
||||
'class' => array_merge(
|
||||
['nav-link'],
|
||||
[!empty($navItem['active']) ? 'active' : ''],
|
||||
[!empty($navItem['disabled']) ? 'disabled' : '']
|
||||
),
|
||||
'data-bs-toggle' => $this->options['pills'] ? 'pill' : 'tab',
|
||||
'id' => $navItem['id'] . '-tab',
|
||||
'href' => '#' . $navItem['id'],
|
||||
'aria-controls' => $navItem['id'],
|
||||
'aria-selected' => !empty($navItem['active']),
|
||||
'role' => 'tab',
|
||||
]);
|
||||
$html .= $navItem['html'] ?? h($navItem['text']);
|
||||
$html .= $this->nodeClose('a');
|
||||
$html .= $this->nodeClose('li');
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genContent(): string
|
||||
{
|
||||
$html = $this->nodeOpen('div', [
|
||||
'class' => array_merge(['tab-content'], $this->options['content-class']),
|
||||
]);
|
||||
foreach ($this->data['content'] as $i => $content) {
|
||||
$navItem = $this->data['navs'][$i];
|
||||
$html .= $this->genContentItem($navItem, $content);
|
||||
}
|
||||
$html .= $this->nodeClose('div');
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genContentItem(array $navItem, string $content): string
|
||||
{
|
||||
return $this->node('div', [
|
||||
'class' => array_merge(['tab-pane', 'fade'], [!empty($navItem['active']) ? 'show active' : '']),
|
||||
'role' => 'tabpanel',
|
||||
'id' => $navItem['id'],
|
||||
'aria-labelledby' => $navItem['id'] . '-tab'
|
||||
], $content);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper\BootstrapElements;
|
||||
|
||||
use App\View\Helper\BootstrapGeneric;
|
||||
|
||||
/**
|
||||
* Creates a bootstrap toast by calling creating a Toaster object and passing the provided options
|
||||
*
|
||||
* # Options:
|
||||
* - text: The text content of the alert
|
||||
* - html: The HTML content of the alert
|
||||
* - dismissible: Can the alert be dissmissed
|
||||
* - variant: The Bootstrap variant of the alert
|
||||
* - fade: Should the alert fade when dismissed
|
||||
* - class: Additional classes to add to the alert container
|
||||
*
|
||||
* # Usage:
|
||||
* $this->Bootstrap->toast([
|
||||
* 'title' => 'Title',
|
||||
* 'bodyHtml' => '<i>Body</i>',
|
||||
* 'muted' => 'Muted text',
|
||||
* 'variant' => 'warning',
|
||||
* 'closeButton' => true,
|
||||
* ]);
|
||||
*/
|
||||
class BootstrapToast extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'id' => false,
|
||||
'title' => false,
|
||||
'muted' => false,
|
||||
'body' => false,
|
||||
'variant' => 'default',
|
||||
'autohide' => true,
|
||||
'delay' => 'auto',
|
||||
'titleHtml' => false,
|
||||
'mutedHtml' => false,
|
||||
'bodyHtml' => false,
|
||||
'closeButton' => true,
|
||||
];
|
||||
|
||||
function __construct(array $options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => array_merge(BootstrapGeneric::$variants, ['default']),
|
||||
];
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
private function processOptions(array $options): void
|
||||
{
|
||||
$validOptions = array_filter($options, function($optionName) {
|
||||
return isset($this->defaultOptions[$optionName]);
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
$this->options = array_merge($this->defaultOptions, $validOptions);
|
||||
$this->checkOptionValidity();
|
||||
}
|
||||
|
||||
public function toast(): string
|
||||
{
|
||||
return $this->genToast();
|
||||
}
|
||||
|
||||
private function genToast(): string
|
||||
{
|
||||
return $this->node('script', [], sprintf(
|
||||
"$(document).ready(function() {
|
||||
UI.toast(%s);
|
||||
})",
|
||||
json_encode($this->options, JSON_FORCE_OBJECT)
|
||||
));
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -18,22 +18,22 @@ class SocialProviderHelper extends Helper
|
|||
return !empty($identity['social_profile']);
|
||||
}
|
||||
|
||||
public function getIcon($identity)
|
||||
public function getIcon($identity, array $classes=[])
|
||||
{
|
||||
if (!empty($identity['social_profile'])) {
|
||||
$provider = $identity['social_profile']['provider'];
|
||||
if (!empty($this->providerImageMapping[$provider])) {
|
||||
return $this->genImage($this->providerImageMapping[$provider], h($provider));
|
||||
return $this->genImage($this->providerImageMapping[$provider], h($provider), $classes);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function genImage($url, $alt)
|
||||
private function genImage($url, $alt, array $classes=[])
|
||||
{
|
||||
return $this->Bootstrap->genNode('img', [
|
||||
return $this->Bootstrap->node('img', [
|
||||
'src' => $url,
|
||||
'class' => ['img-fluid'],
|
||||
'class' => array_merge(['img-fluid'], $classes),
|
||||
'width' => '16',
|
||||
'height' => '16',
|
||||
'alt' => $alt,
|
||||
|
|
|
@ -5,6 +5,16 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'top_bar' => [
|
||||
'pull' => 'right',
|
||||
'children' => [
|
||||
[
|
||||
'type' => 'simple',
|
||||
'children' => [
|
||||
'data' => [
|
||||
'type' => 'simple',
|
||||
'text' => __('Download All'),
|
||||
'popover_url' => sprintf('/broods/downloadIndividual/%s/all', h($brood_id)),
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Search'),
|
||||
|
@ -21,6 +31,13 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'sort' => 'id',
|
||||
'data_path' => 'id',
|
||||
],
|
||||
[
|
||||
'name' => __('Status'),
|
||||
'class' => 'short',
|
||||
'data_path' => 'status',
|
||||
'sort' => 'status',
|
||||
'element' => 'brood_sync_status',
|
||||
],
|
||||
[
|
||||
'name' => __('Email'),
|
||||
'sort' => 'email',
|
||||
|
@ -53,8 +70,8 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'pull' => 'right',
|
||||
'actions' => [
|
||||
[
|
||||
'url' => '/broods/downloadIndividual/' . $brood_id,
|
||||
'url_params_data_paths' => ['id'],
|
||||
'open_modal' => '/broods/downloadIndividual/' . $brood_id . '/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'title' => __('Download'),
|
||||
'icon' => 'download'
|
||||
]
|
||||
|
|
|
@ -3,8 +3,17 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'data' => [
|
||||
'data' => $data,
|
||||
'top_bar' => [
|
||||
'pull' => 'right',
|
||||
'children' => [
|
||||
[
|
||||
'type' => 'simple',
|
||||
'children' => [
|
||||
'data' => [
|
||||
'type' => 'simple',
|
||||
'text' => __('Download All'),
|
||||
'popover_url' => sprintf('/broods/downloadOrg/%s/all', h($brood_id)),
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Search'),
|
||||
|
@ -22,6 +31,14 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'class' => 'short',
|
||||
'data_path' => 'id',
|
||||
],
|
||||
[
|
||||
'name' => __('Status'),
|
||||
'class' => 'short',
|
||||
'data_path' => 'status',
|
||||
'display_field_data_path' => 'name',
|
||||
'sort' => 'status',
|
||||
'element' => 'brood_sync_status',
|
||||
],
|
||||
[
|
||||
'name' => __('Name'),
|
||||
'class' => 'short',
|
||||
|
@ -58,8 +75,8 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'pull' => 'right',
|
||||
'actions' => [
|
||||
[
|
||||
'url' => '/broods/downloadOrg/' . $brood_id,
|
||||
'url_params_data_paths' => ['id'],
|
||||
'open_modal' => '/broods/downloadOrg/' . $brood_id . '/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'title' => __('Download'),
|
||||
'icon' => 'download'
|
||||
]
|
||||
|
|
|
@ -5,6 +5,16 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'top_bar' => [
|
||||
'pull' => 'right',
|
||||
'children' => [
|
||||
[
|
||||
'type' => 'simple',
|
||||
'children' => [
|
||||
'data' => [
|
||||
'type' => 'simple',
|
||||
'text' => __('Download All'),
|
||||
'popover_url' => sprintf('/broods/downloadSharingGroup/%s/all', h($brood_id)),
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Search'),
|
||||
|
@ -22,6 +32,13 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'class' => 'short',
|
||||
'data_path' => 'id',
|
||||
],
|
||||
[
|
||||
'name' => __('Status'),
|
||||
'class' => 'short',
|
||||
'data_path' => 'status',
|
||||
'sort' => 'status',
|
||||
'element' => 'brood_sync_status',
|
||||
],
|
||||
[
|
||||
'name' => __('Name'),
|
||||
'class' => 'short',
|
||||
|
@ -38,8 +55,8 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'pull' => 'right',
|
||||
'actions' => [
|
||||
[
|
||||
'url' => '/broods/downloadSharingGroup/' . $brood_id,
|
||||
'url_params_data_paths' => ['id'],
|
||||
'open_modal' => '/broods/downloadSharingGroup/' . $brood_id . '/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'title' => __('Download'),
|
||||
'icon' => 'download'
|
||||
]
|
||||
|
|
|
@ -49,7 +49,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'element' => 'owner'
|
||||
],
|
||||
[
|
||||
'name' => __('Revoked'),
|
||||
'name' => __('Fingerprint'),
|
||||
'data_path' => 'fingerprint'
|
||||
],
|
||||
[
|
||||
|
|
|
@ -9,9 +9,7 @@ if (!empty($updateAvailables)) {
|
|||
'icon' => 'arrow-alt-circle-up',
|
||||
'class' => 'mt-1',
|
||||
'text' => __n('Run update', 'Run all updates', count($updateAvailables)),
|
||||
'params' => [
|
||||
'onclick' => 'runAllUpdate()'
|
||||
]
|
||||
'onclick' => 'runAllUpdate()',
|
||||
])
|
||||
);
|
||||
echo $this->Bootstrap->alert([
|
||||
|
@ -35,11 +33,11 @@ foreach ($status as $i => &$update) {
|
|||
|
||||
echo $this->Bootstrap->table([], [
|
||||
'fields' => [
|
||||
['key' => 'id', 'label' => __('ID')],
|
||||
['key' => 'name', 'label' => __('Name')],
|
||||
['key' => 'end_time', 'label' => __('End Time')],
|
||||
['key' => 'time_taken_formated', 'label' => __('Time Taken')],
|
||||
['key' => 'status', 'label' => __('Status')]
|
||||
['path' => 'id', 'label' => __('ID')],
|
||||
['path' => 'name', 'label' => __('Name')],
|
||||
['path' => 'end_time', 'label' => __('End Time')],
|
||||
['path' => 'time_taken_formated', 'label' => __('Time Taken')],
|
||||
['path' => 'status', 'label' => __('Status')]
|
||||
],
|
||||
'items' => $status,
|
||||
]);
|
||||
|
|
|
@ -29,7 +29,7 @@ foreach ($entities as $entity) {
|
|||
);
|
||||
}
|
||||
if ($amountOfEntitiesToUpdate > 10) {
|
||||
$bodyHtml .= sprintf('<li class="list-inline-item fw-light fs-7">%s</li>', __('{0} more entities', h(10 - $amountOfEntitiesToUpdate)));
|
||||
$bodyHtml .= sprintf('<li class="list-inline-item fw-light fs-7">%s</li>', __('{0} more entities', $amountOfEntitiesToUpdate - 10));
|
||||
}
|
||||
$bodyHtml .= '</ul>';
|
||||
|
||||
|
|
|
@ -18,9 +18,7 @@ if (!empty($updateableTemplates['new'])) {
|
|||
'size' => 'sm',
|
||||
'icon' => 'download',
|
||||
'title' => __('Create this template'),
|
||||
'params' => [
|
||||
'onclick' => "UI.submissionModalForIndex('/metaTemplates/createNewTemplate/{$entry['uuid']}', '/meta-templates')"
|
||||
]
|
||||
'onclick' => "UI.submissionModalForIndex('/metaTemplates/createNewTemplate/{$entry['uuid']}', '/meta-templates')",
|
||||
])
|
||||
);
|
||||
}, $alertList);
|
||||
|
@ -200,10 +198,22 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
'open_modal' => '/metaTemplates/migrateMetafieldsToNewestTemplate/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'title' => __('Update meta-fields to the newest version of this meta-template'),
|
||||
'icon' => 'arrow-circle-up',
|
||||
'variant' => 'success',
|
||||
'complex_requirement' => [
|
||||
'function' => function ($row, $options) {
|
||||
return !empty($row['updateStatus']['to-existing']) && empty($row['updateStatus']['can-be-removed']);
|
||||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
'open_modal' => '/metaTemplates/delete/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'title' => __('Get meta-fields that should be moved to the newest version of this meta-template'),
|
||||
'title' => __('This meta-template doesn\'t have any meta-fields and can be safely removed.'),
|
||||
'icon' => 'trash',
|
||||
'variant' => 'success',
|
||||
'complex_requirement' => [
|
||||
|
@ -225,4 +235,3 @@ function getConflictingTemplate($row, $data) {
|
|||
}
|
||||
return [];
|
||||
}
|
||||
?>
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
|
||||
use Cake\Utility\Inflector;
|
||||
use Cake\Routing\Router;
|
||||
|
||||
$urlNewestMetaTemplate = Router::url([
|
||||
'controller' => 'metaTemplates',
|
||||
'action' => 'view',
|
||||
$newestMetaTemplate->id
|
||||
]);
|
||||
|
||||
$bodyHtml = '';
|
||||
$bodyHtml .= sprintf('<div><span>%s: </span><span class="font-monospace">%s</span></div>', __('Current version'), h($oldMetaTemplate->version));
|
||||
$bodyHtml .= sprintf('<div><span>%s: </span><a href="%s" target="_blank" class="font-monospac">%s</a></div>', __('Newest version'), $urlNewestMetaTemplate, h($newestMetaTemplate->version));
|
||||
$bodyHtml .= sprintf('<h4 class="my-2">%s</h4>', __('{0} Entities with meta-fields for the meta-template version <span class="font-monospace">{1}</span>', h($entityCount), h($oldMetaTemplate->version)));
|
||||
|
||||
if (empty($conflictingEntities)) {
|
||||
$bodyHtml .= $this->Bootstrap->alert([
|
||||
'text' => __('All entities can updated automatically', count($conflictingEntities)),
|
||||
'variant' => 'success',
|
||||
'dismissible' => false,
|
||||
]);
|
||||
} else {
|
||||
$bodyHtml .= $this->Bootstrap->alert([
|
||||
'html' => sprintf(
|
||||
'<ul>%s%s</ul>',
|
||||
$this->Bootstrap->node('li', [], __('{0} entities can be updated automatically', $entityCount - count($conflictingEntities))),
|
||||
$this->Bootstrap->node('li', [], __('{0} entities cannot be updated automatically and require manual migration', count($conflictingEntities)))
|
||||
),
|
||||
'variant' => 'warning',
|
||||
'dismissible' => false,
|
||||
]);
|
||||
$bodyHtml .= '<ul>';
|
||||
foreach ($conflictingEntities as $i => $entity) {
|
||||
$url = Router::url([
|
||||
'controller' => 'metaTemplates',
|
||||
'action' => 'migrateOldMetaTemplateToNewestVersionForEntity',
|
||||
$oldMetaTemplate->id,
|
||||
$entity->id,
|
||||
]);
|
||||
$bodyHtml .= sprintf(
|
||||
'<li><a href="%s" target="_blank">%s</a> <span class="fw-light">%s<span></li>',
|
||||
$url,
|
||||
__('{0}::{1}', h(Inflector::humanize($oldMetaTemplate->scope)), $entity->id),
|
||||
__('has {0} meta-fields to update', count($entity->meta_fields))
|
||||
);
|
||||
if ($i >= 9) {
|
||||
$bodyHtml .= sprintf('<li class="list-inline-item fw-light fs-7">%s</li>', __('{0} more entities', count($conflictingEntities) - 10));
|
||||
break;
|
||||
}
|
||||
}
|
||||
$bodyHtml .= '</ul>';
|
||||
}
|
||||
$form = sprintf(
|
||||
'<div class="d-none hidden-form-container">%s%s</div>',
|
||||
$this->Form->create(null, [
|
||||
'url' => [
|
||||
'controller' => 'MetaTemplates',
|
||||
'action' => 'migrateMetafieldsToNewestTemplate',
|
||||
$oldMetaTemplate->id,
|
||||
]
|
||||
]),
|
||||
$this->Form->end()
|
||||
);
|
||||
$bodyHtml .= $form;
|
||||
$form = sprintf(
|
||||
'<div class="d-none hidden-form-force-container">%s%s</div>',
|
||||
$this->Form->create(null, [
|
||||
'url' => [
|
||||
'controller' => 'MetaTemplates',
|
||||
'action' => 'migrateMetafieldsToNewestTemplate',
|
||||
$oldMetaTemplate->id,
|
||||
1,
|
||||
]
|
||||
]),
|
||||
$this->Form->end()
|
||||
);
|
||||
$bodyHtml .= $form;
|
||||
|
||||
$title = __('{0} has a new meta-template and meta-fields to be updated', sprintf('<i class="me-1">%s</i>', h($oldMetaTemplate->name)));
|
||||
if (!empty($ajax)) {
|
||||
echo $this->Bootstrap->modal([
|
||||
'titleHtml' => $title,
|
||||
'bodyHtml' => $bodyHtml,
|
||||
'size' => 'lg',
|
||||
'type' => 'custom',
|
||||
'footerButtons' => [
|
||||
[
|
||||
'text' => __('Cancel'),
|
||||
'variant' => 'secondary',
|
||||
],
|
||||
[
|
||||
'text' => __('Force Migrate meta-fields'),
|
||||
'title' => __('Any meta-field having conflict will be deleted.'),
|
||||
'variant' => 'danger',
|
||||
'clickFunction' => 'forceMigrateMetafieldsToNewestTemplate',
|
||||
],
|
||||
[
|
||||
'text' => __('Migrate meta-fields'),
|
||||
'variant' => 'success',
|
||||
'clickFunction' => 'migrateMetafieldsToNewestTemplate',
|
||||
],
|
||||
],
|
||||
]);
|
||||
} else {
|
||||
echo $this->Bootstrap->node('h1', [], $title);
|
||||
echo $bodyHtml;
|
||||
echo $this->Bootstrap->button([
|
||||
'text' => __('Force Migrate meta-fields'),
|
||||
'title' => __('Any meta-field having conflict will be deleted.'),
|
||||
'variant' => 'danger',
|
||||
'class' => ['me-2'],
|
||||
'onclick' => '$(".hidden-form-force-container form").submit()',
|
||||
]);
|
||||
echo $this->Bootstrap->button([
|
||||
'text' => __('Migrate meta-fields'),
|
||||
'variant' => 'success',
|
||||
'onclick' => '$(".hidden-form-container form").submit()',
|
||||
]);
|
||||
}
|
||||
?>
|
||||
|
||||
<script>
|
||||
function migrateMetafieldsToNewestTemplate(modalObject, tmpApi) {
|
||||
const $form = modalObject.$modal.find('.hidden-form-container form')
|
||||
return doMigration($form, modalObject, tmpApi)
|
||||
}
|
||||
|
||||
function forceMigrateMetafieldsToNewestTemplate(modalObject, tmpApi) {
|
||||
return UI.quickConfirm(tmpApi.options.statusNode, {
|
||||
variant: 'danger',
|
||||
title: '<?= __('Confirm potential deletion of data') ?>',
|
||||
description: '<?= __('By chosing to force the migration, any meta-fields not satisfying the validation requirements will be deleted.') ?>',
|
||||
confirmText: '<?= __('Force migration') ?>',
|
||||
confirm: function() {
|
||||
const $form = modalObject.$modal.find('.hidden-form-force-container form')
|
||||
return doMigration($form, modalObject, tmpApi)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function doMigration($form, modalObject, tmpApi) {
|
||||
return tmpApi.postForm($form[0]).catch((errors) => {
|
||||
const formHelper = new FormValidationHelper($form[0])
|
||||
const errorHTMLNode = formHelper.buildValidationMessageNode(errors, true)
|
||||
modalObject.$modal.find('div.form-error-container').append(errorHTMLNode)
|
||||
return errors
|
||||
})
|
||||
}
|
||||
</script>
|
|
@ -71,9 +71,7 @@ use Cake\Routing\Router;
|
|||
$this->Bootstrap->button([
|
||||
'text' => __('Update to version {0}', h($newMetaTemplate->version)),
|
||||
'variant' => 'success',
|
||||
'params' => [
|
||||
'onclick' => 'submitMigration()'
|
||||
]
|
||||
'onclick' => 'submitMigration()',
|
||||
])
|
||||
?>
|
||||
</div>
|
||||
|
|
|
@ -18,6 +18,11 @@ if ($updateStatus['up-to-date']) {
|
|||
'html' => __('This meta-template can be updated to version {0} (current: {1}).', sprintf('<strong>%s</strong>', h($templateOnDisk['version'])), h($metaTemplate->version)),
|
||||
'dismissible' => false,
|
||||
]);
|
||||
$bodyHtml .= $this->Bootstrap->alert([
|
||||
'variant' => 'success',
|
||||
'text' => __('All meta-fields will be migrated to the newest version.'),
|
||||
'dismissible' => false,
|
||||
]);
|
||||
$form = $this->element('genericElements/Form/genericForm', [
|
||||
'entity' => null,
|
||||
'ajax' => false,
|
||||
|
@ -28,9 +33,9 @@ if ($updateStatus['up-to-date']) {
|
|||
[
|
||||
'field' => 'update_strategy',
|
||||
'type' => 'checkbox',
|
||||
'value' => MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW,
|
||||
'value' => MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING,
|
||||
'checked' => true,
|
||||
]
|
||||
],
|
||||
],
|
||||
'submit' => [
|
||||
'action' => $this->request->getParam('action')
|
||||
|
@ -42,7 +47,7 @@ if ($updateStatus['up-to-date']) {
|
|||
$modalSize = 'xl';
|
||||
$bodyHtml .= $this->Bootstrap->alert([
|
||||
'variant' => 'warning',
|
||||
'text' => __('Updating to version {0} cannot be done automatically as it introduces some conflicts.', h($templateOnDisk['version'])),
|
||||
'html' => __('Updating to version {0} cannot be done automatically as it introduces some conflicts.', sprintf('<strong>%s</strong>', h($templateOnDisk['version']))),
|
||||
'dismissible' => false,
|
||||
]);
|
||||
$conflictTable = $this->element('MetaTemplates/conflictTable', [
|
||||
|
@ -50,8 +55,11 @@ if ($updateStatus['up-to-date']) {
|
|||
'metaTemplate' => $metaTemplate,
|
||||
'templateOnDisk' => $templateOnDisk,
|
||||
]);
|
||||
$conflictCount = array_reduce($templateStatus['conflicts'], function ($carry, $conflict) {
|
||||
return $carry + count($conflict['conflictingEntities']);
|
||||
}, 0);
|
||||
$bodyHtml .= $this->Bootstrap->collapse([
|
||||
'title' => __('View conflicts'),
|
||||
'text' => __('View conflicts ({0})', $conflictCount),
|
||||
'open' => false
|
||||
], $conflictTable);
|
||||
$bodyHtml .= $this->element('MetaTemplates/conflictResolution', [
|
||||
|
|
|
@ -40,12 +40,19 @@ foreach ($templatesUpdateStatus as $uuid => $status) {
|
|||
if (!empty($status['new'])) {
|
||||
$tableHtml .= sprintf('<td>%s</td>', __('N/A'));
|
||||
} else {
|
||||
$tableHtml .= sprintf(
|
||||
'<td>%s %s %s</td>',
|
||||
h($status['current_version']),
|
||||
$this->Bootstrap->icon('arrow-right', ['class' => 'fs-8']),
|
||||
h($status['next_version'])
|
||||
);
|
||||
if ($status['current_version'] == $status['next_version']) {
|
||||
$tableHtml .= sprintf(
|
||||
'<td>%s</td>',
|
||||
h($status['current_version'])
|
||||
);
|
||||
} else {
|
||||
$tableHtml .= sprintf(
|
||||
'<td>%s %s %s</td>',
|
||||
h($status['current_version']),
|
||||
$this->Bootstrap->icon('arrow-right', ['class' => 'fs-8']),
|
||||
h($status['next_version'])
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!empty($status['new'])) {
|
||||
$numberOfUpdates += 1;
|
||||
|
@ -60,6 +67,8 @@ foreach ($templatesUpdateStatus as $uuid => $status) {
|
|||
}
|
||||
if (!empty($status['new'])) {
|
||||
$tableHtml .= sprintf('<td>%s</td>', __('N/A'));
|
||||
} elseif (!empty($status['up-to-date'])) {
|
||||
$tableHtml .= sprintf('<td>%s</td>', __('N/A'));
|
||||
} else {
|
||||
$tableHtml .= sprintf('<td>%s</td>', !empty($status['conflicts']) ? $this->Bootstrap->icon('check') : $this->Bootstrap->icon('times'));
|
||||
}
|
||||
|
|
|
@ -145,6 +145,12 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
]
|
||||
],
|
||||
'function' => function ($row, $options) use ($loggedUser, $validRoles) {
|
||||
if (empty(Configure::read('user.allow-user-deletion'))) {
|
||||
return false;
|
||||
}
|
||||
if ($row['id'] == $loggedUser['id']) {
|
||||
return false;
|
||||
}
|
||||
if (empty($loggedUser['role']['perm_admin'])) {
|
||||
if (empty($loggedUser['role']['perm_org_admin'])) {
|
||||
return false;
|
||||
|
|
|
@ -20,7 +20,7 @@ use Cake\Core\Configure;
|
|||
$this->Form->setTemplates($template);
|
||||
if (!Configure::check('password_auth.enabled') || Configure::read('password_auth.enabled')) {
|
||||
echo sprintf('<h4 class="text-uppercase fw-light mb-3">%s</h4>', __('Sign In'));
|
||||
echo $this->Form->create(null, ['url' => ['controller' => 'users', 'action' => 'login']]);
|
||||
echo $this->Form->create(null, ['url' => ['controller' => 'users', 'action' => 'login', '?' => ['redirect' => $this->request->getQuery('redirect')],]]);
|
||||
echo $this->Form->control('username', ['label' => 'Username', 'class' => 'form-control mb-2', 'placeholder' => __('Username')]);
|
||||
echo $this->Form->control('password', ['type' => 'password', 'label' => 'Password', 'class' => 'form-control mb-3', 'placeholder' => __('Password')]);
|
||||
echo $this->Form->control(__('Login'), ['type' => 'submit', 'class' => 'btn btn-primary']);
|
||||
|
@ -52,8 +52,8 @@ use Cake\Core\Configure;
|
|||
'class' => ['d-block', 'w-100'],
|
||||
'image' => [
|
||||
'path' => '/img/keycloak_logo.png',
|
||||
'alt' => 'Keycloak'
|
||||
]
|
||||
'alt' => 'Keycloak',
|
||||
],
|
||||
]);
|
||||
echo $this->Form->end();
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<?php
|
||||
$create_new_allowed = true;
|
||||
$keep_all_allowed = false;
|
||||
$delete_all_allowed = false;
|
||||
$totalAllowed = $create_new_allowed + $keep_all_allowed + $delete_all_allowed;
|
||||
$maxWidth = 99 - ($create_new_allowed ? 33 : 0) - ($keep_all_allowed ? 33 : 0) - ($delete_all_allowed ? 33 : 0);
|
||||
$update_allowed = true;
|
||||
$delete_all_allowed = true;
|
||||
$totalAllowed = $create_new_allowed + $update_allowed + $delete_all_allowed;
|
||||
$maxWidth = 99 - ($create_new_allowed ? 33 : 0) - ($update_allowed ? 33 : 0) - ($delete_all_allowed ? 33 : 0);
|
||||
$defaultStrategy = 'update_existing';
|
||||
|
||||
$form = $this->element('genericElements/Form/genericForm', [
|
||||
'entity' => null,
|
||||
|
@ -17,8 +18,8 @@ $form = $this->element('genericElements/Form/genericForm', [
|
|||
'type' => 'radio',
|
||||
'options' => [
|
||||
['value' => 'create_new', 'text' => 'create_new', 'id' => 'radio_create_new'],
|
||||
['value' => 'keep_both', 'text' => 'keep_both', 'id' => 'radio_keep_both'],
|
||||
['value' => 'delete', 'text' => 'delete', 'id' => 'radio_delete'],
|
||||
['value' => 'update_existing', 'text' => 'update', 'id' => 'radio_update'],
|
||||
['value' => 'delete_all', 'text' => 'delete_all', 'id' => 'radio_delete_all'],
|
||||
],
|
||||
]
|
||||
],
|
||||
|
@ -32,11 +33,30 @@ $form = $this->element('genericElements/Form/genericForm', [
|
|||
<div class="conflict-resolution-picker">
|
||||
<div class="mt-3 d-flex justify-content-center">
|
||||
<div class="btn-group justify-content-center" role="group" aria-label="Basic radio toggle button group">
|
||||
<?php if ($create_new_allowed) : ?>
|
||||
<input type="radio" class="btn-check" name="btnradio" id="btnradio1" autocomplete="off" value="create_new" checked>
|
||||
<label class="btn btn-outline-primary mw-<?= $maxWidth ?>" for="btnradio1">
|
||||
<?php if ($delete_all_allowed) : ?>
|
||||
<input type="radio" class="btn-check" name="btnradio" id="btnradio3" autocomplete="off" value="delete" <?= $defaultStrategy == 'delete' ? 'checked' : '' ?>>
|
||||
<label class="btn btn-outline-danger mw-<?= $maxWidth ?>" for="btnradio3">
|
||||
<div>
|
||||
<h5 class="mb-3"><?= __('Create new template') ?></h5>
|
||||
<h5 class="mb-3">
|
||||
<?= $defaultStrategy == 'delete' ? $this->Bootstrap->badge(['text' => 'recommended', 'variant' => 'success', 'class' => ['mb-3', 'fs-8']]) : '' ?>
|
||||
<?= __('Delete conflicting fields') ?>
|
||||
</h5>
|
||||
<ul class="text-start fs-7">
|
||||
<li><?= __('Meta-fields not satisfying the new meta-template definition will be deleted.') ?></li>
|
||||
<li><?= __('All other meta-fields will be upgraded to the new meta-template.') ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($create_new_allowed) : ?>
|
||||
<input type="radio" class="btn-check" name="btnradio" id="btnradio1" autocomplete="off" value="create_new" <?= $defaultStrategy == 'create_new' ? 'checked' : '' ?>>
|
||||
<label class="btn btn-outline-warning mw-<?= $maxWidth ?>" for="btnradio1">
|
||||
<div>
|
||||
<h5 class="mb-3">
|
||||
<?= $defaultStrategy == 'create_new' ? $this->Bootstrap->badge(['text' => 'recommended', 'variant' => 'success', 'class' => ['mb-3', 'fs-8']]) : '' ?>
|
||||
<?= __('Create new template') ?>
|
||||
</h5>
|
||||
<ul class="text-start fs-7">
|
||||
<li><?= __('A new meta-template will be created and made default.') ?></li>
|
||||
<li><?= __('The old meta-template will remain untouched.') ?></li>
|
||||
|
@ -46,32 +66,22 @@ $form = $this->element('genericElements/Form/genericForm', [
|
|||
</label>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($keep_all_allowed) : ?>
|
||||
<input type="radio" class="btn-check" name="btnradio" id="btnradio2" autocomplete="off" value="keep_both">
|
||||
<label class="btn btn-outline-warning mw-<?= $maxWidth ?>" for="btnradio2">
|
||||
<?php if ($update_allowed) : ?>
|
||||
<input type="radio" class="btn-check" name="btnradio" id="btnradio2" autocomplete="off" value="update_existing" <?= $defaultStrategy == 'update_existing' ? 'checked' : '' ?>>
|
||||
<label class="btn btn-outline-primary mw-<?= $maxWidth ?>" for="btnradio2">
|
||||
<div>
|
||||
<h5 class="mb-3"><?= __('Update non-conflicting') ?></h5>
|
||||
<div><?= $defaultStrategy == 'update_existing' ? $this->Bootstrap->badge(['text' => 'recommended', 'variant' => 'success', 'class' => ['mb-3']]) : '' ?></div>
|
||||
<h5 class="mb-3">
|
||||
<?= __('Update non-conflicting') ?>
|
||||
</h5>
|
||||
<ul class="text-start fs-7">
|
||||
<li><?= __('Meta-fields not having conflicts will be migrated to the new meta-template.') ?></li>
|
||||
<li><?= __('Meta-fields having a conflicts will stay on their current meta-template.') ?></li>
|
||||
<li><?= __('Entities not having conflicts will have their meta-fields migrated to the new meta-template.') ?></li>
|
||||
<li><?= __('Entities having a conflicts will stay on their current meta-template.') ?></li>
|
||||
<li><?= __('Conflicts can be taken care of manually via the UI.') ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($delete_all_allowed) : ?>
|
||||
<input type="radio" class="btn-check" name="btnradio" id="btnradio3" autocomplete="off" value="delete">
|
||||
<label class="btn btn-outline-danger mw-<?= $maxWidth ?>" for="btnradio3">
|
||||
<div>
|
||||
<h5 class="mb-3"><?= __('Delete conflicting fields') ?></h5>
|
||||
<ul class="text-start fs-7">
|
||||
<li><?= __('Meta-fields not satisfying the new meta-template definition will be deleted.') ?></li>
|
||||
<li><?= __('All other meta-fields will be upgraded to the new meta-template.') ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -84,18 +94,18 @@ $form = $this->element('genericElements/Form/genericForm', [
|
|||
(function() {
|
||||
const $form = $('.conflict-resolution-form-container form')
|
||||
const $create = $form.find('input#radio_create_new')
|
||||
const $keep = $form.find('input#radio_keep_both')
|
||||
const $delete = $form.find('input#radio_delete')
|
||||
const $keep = $form.find('input#radio_update')
|
||||
const $delete = $form.find('input#radio_delete_all')
|
||||
|
||||
$(document).ready(function() {
|
||||
$('.conflict-resolution-picker').find('input[type="radio"]').change(function() {
|
||||
updateSelected($(this).val())
|
||||
})
|
||||
updateSelected('create_new')
|
||||
updateSelected('<?= $defaultStrategy ?>')
|
||||
})
|
||||
|
||||
function updateSelected(choice) {
|
||||
if (choice == 'keep_both') {
|
||||
if (choice == 'update_existing') {
|
||||
$keep.prop('checked', true)
|
||||
} else if (choice == 'delete') {
|
||||
$delete.prop('checked', true)
|
||||
|
|
|
@ -21,7 +21,7 @@ use Cake\Routing\Router;
|
|||
</td>
|
||||
<td>
|
||||
<?php
|
||||
foreach ($fieldConflict['conflictingEntities'] as $i => $id) {
|
||||
foreach ($fieldConflict['conflictingEntities'] as $i => $metaEntity) {
|
||||
if ($i > 0) {
|
||||
echo ', ';
|
||||
}
|
||||
|
@ -32,9 +32,9 @@ use Cake\Routing\Router;
|
|||
$url = Router::url([
|
||||
'controller' => Inflector::pluralize($templateStatus['existing_template']->scope),
|
||||
'action' => 'view',
|
||||
$id
|
||||
$metaEntity['parent_id']
|
||||
]);
|
||||
echo sprintf('<a href="%s" target="_blank">%s</a>', $url, __('{0} #{1}', h(Inflector::humanize($templateStatus['existing_template']->scope)), h($id)));
|
||||
echo sprintf('<a href="%s" target="_blank">%s</a>', $url, __('{0} #{1}', h(Inflector::humanize($templateStatus['existing_template']->scope)), h($metaEntity['parent_id'])));
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
if ($setting['type'] == 'string' || $setting['type'] == 'textarea' || empty($setting['type'])) {
|
||||
$input = (function ($settingName, $setting, $appView) {
|
||||
$settingId = str_replace('.', '_', $settingName);
|
||||
return $appView->Bootstrap->genNode(
|
||||
return $appView->Bootstrap->node(
|
||||
$setting['type'] == 'textarea' ? 'textarea' : 'input',
|
||||
[
|
||||
'class' => [
|
||||
|
@ -43,7 +43,7 @@
|
|||
} elseif ($setting['type'] == 'integer') {
|
||||
$input = (function ($settingName, $setting, $appView) {
|
||||
$settingId = str_replace('.', '_', $settingName);
|
||||
return $appView->Bootstrap->genNode('input', [
|
||||
return $appView->Bootstrap->node('input', [
|
||||
'class' => [
|
||||
'form-control',
|
||||
(!empty($setting['error']) ? 'is-invalid' : ''),
|
||||
|
@ -73,7 +73,7 @@
|
|||
}
|
||||
}
|
||||
$options = [];
|
||||
$options[] = $appView->Bootstrap->genNode('option', ['value' => '-1', 'data-is-empty-option' => '1'], __('Select an option'));
|
||||
$options[] = $appView->Bootstrap->node('option', ['value' => '-1', 'data-is-empty-option' => '1'], __('Select an option'));
|
||||
foreach ($setting['options'] as $key => $value) {
|
||||
$optionParam = [
|
||||
'class' => [],
|
||||
|
@ -88,10 +88,10 @@
|
|||
$optionParam['selected'] = 'selected';
|
||||
}
|
||||
}
|
||||
$options[] = $appView->Bootstrap->genNode('option', $optionParam, h($value));
|
||||
$options[] = $appView->Bootstrap->node('option', $optionParam, h($value));
|
||||
}
|
||||
$options = implode('', $options);
|
||||
return $appView->Bootstrap->genNode('select', [
|
||||
return $appView->Bootstrap->node('select', [
|
||||
'class' => [
|
||||
'form-select',
|
||||
'pe-4',
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
$dependsOnHtml = '';
|
||||
if (!empty($setting['dependsOn'])) {
|
||||
$dependsOnHtml = $this->Bootstrap->genNode('span', [
|
||||
$dependsOnHtml = $this->Bootstrap->node('span', [
|
||||
'class' => [
|
||||
'ms-1',
|
||||
'd-inline-block',
|
||||
|
@ -11,18 +11,18 @@
|
|||
],
|
||||
'style' => 'min-width: 0.75em;',
|
||||
'title' => __('This setting depends on the validity of: {0}', h($setting['dependsOn'])),
|
||||
], $this->Bootstrap->genNode('sup', [
|
||||
], $this->Bootstrap->node('sup', [
|
||||
'class' => $this->FontAwesome->getClass('info'),
|
||||
]));
|
||||
}
|
||||
$label = $this->Bootstrap->genNode('label', [
|
||||
$label = $this->Bootstrap->node('label', [
|
||||
'class' => ['form-label', 'fw-bolder', 'mb-0'],
|
||||
'for' => $settingId
|
||||
], sprintf('<a id="lb-%s" href="#lb-%s" class="text-reset text-decoration-none">%s</a>', h($settingId), h($settingId), h($setting['name'])) . $dependsOnHtml);
|
||||
|
||||
$description = '';
|
||||
if (!empty($setting['description']) && (empty($setting['type']) || $setting['type'] != 'boolean')) {
|
||||
$description = $this->Bootstrap->genNode('small', [
|
||||
$description = $this->Bootstrap->node('small', [
|
||||
'class' => ['form-text', 'text-muted', 'mt-0'],
|
||||
'id' => "{$settingId}Help"
|
||||
], h($setting['description']));
|
||||
|
@ -31,7 +31,7 @@
|
|||
if (!empty($setting['severity'])) {
|
||||
$textColor = "text-{$this->get('variantFromSeverity')[$setting['severity']]}";
|
||||
}
|
||||
$validationError = $this->Bootstrap->genNode('div', [
|
||||
$validationError = $this->Bootstrap->node('div', [
|
||||
'class' => ['d-block', 'invalid-feedback', $textColor],
|
||||
], (!empty($setting['error']) ? h($setting['errorMessage']) : ''));
|
||||
|
||||
|
@ -50,11 +50,11 @@
|
|||
'variant' => 'success',
|
||||
'class' => ['btn-setting-action', 'btn-save-setting', 'd-none'],
|
||||
]);
|
||||
$inputGroup = $this->Bootstrap->genNode('div', [
|
||||
$inputGroup = $this->Bootstrap->node('div', [
|
||||
'class' => ['input-group'],
|
||||
], implode('', [$input, $inputGroupSave]));
|
||||
|
||||
$container = $this->Bootstrap->genNode('div', [
|
||||
$container = $this->Bootstrap->node('div', [
|
||||
'class' => ['setting-group', 'row', 'mb-2']
|
||||
], implode('', [$label, $inputGroup, $description, $validationError]));
|
||||
|
||||
|
|
|
@ -50,14 +50,14 @@ foreach (array_keys($mainNoticeHeading) as $level) {
|
|||
'bordered' => false,
|
||||
], [
|
||||
'fields' => [
|
||||
['key' => 'name', 'label' => __('Name'), 'formatter' => function($name, $row) {
|
||||
['path' => 'name', 'label' => __('Name'), 'formatter' => function($name, $row) {
|
||||
$settingID = preg_replace('/(\.|\W)/', '_', h($row['true-name']));
|
||||
return sprintf('<a style="max-width: 200px; white-space: pre-wrap;" href="#lb-%s" onclick="redirectToSetting(\'#lb-%s\')">%s</a>', $settingID, $settingID, h($name));
|
||||
return sprintf('<a style="max-width: 200px; white-space: nowrap;" href="#lb-%s" onclick="redirectToSetting(\'#lb-%s\')">%s</a>', $settingID, $settingID, h($name));
|
||||
}],
|
||||
['key' => 'setting-path', 'label' => __('Category'), 'formatter' => function($path, $row) {
|
||||
['path' => 'setting-path', 'label' => __('Category'), 'formatter' => function($path, $row) {
|
||||
return '<span class="text-nowrap">' . h(str_replace('.', ' ▸ ', $path)) . '</span>';
|
||||
}],
|
||||
['key' => 'value', 'label' => __('Value'), 'formatter' => function($value, $row) {
|
||||
['path' => 'value', 'label' => __('Value'), 'formatter' => function($value, $row) {
|
||||
$formatedValue = '<span class="p-1 rounded mb-0" style="background: #eeeeee55; font-family: monospace;">';
|
||||
if (is_null($value)) {
|
||||
$formatedValue .= '<i class="text-nowrap">' . __('No value') . '</i>';
|
||||
|
@ -71,7 +71,7 @@ foreach (array_keys($mainNoticeHeading) as $level) {
|
|||
$formatedValue .= '</span>';
|
||||
return $formatedValue;
|
||||
}],
|
||||
['key' => 'description', 'label' => __('Description')]
|
||||
['path' => 'description', 'label' => __('Description')]
|
||||
],
|
||||
'items' => $notices[$level],
|
||||
]);
|
||||
|
@ -87,14 +87,14 @@ $alertBody = $this->Bootstrap->table([
|
|||
'tableClass' => 'mb-0'
|
||||
], [
|
||||
'fields' => [
|
||||
['key' => 'severity', 'label' => __('Severity')],
|
||||
['key' => 'issues', 'label' => __('Issues'), 'formatter' => function($count, $row) {
|
||||
['path' => 'severity', 'label' => __('Severity')],
|
||||
['path' => 'issues', 'label' => __('Issues'), 'formatter' => function($count, $row) {
|
||||
return $this->Bootstrap->badge([
|
||||
'variant' => $row['badge-variant'],
|
||||
'text' => $count
|
||||
]);
|
||||
}],
|
||||
['key' => 'description', 'label' => __('Description')]
|
||||
['path' => 'description', 'label' => __('Description')]
|
||||
],
|
||||
'items' => $tableItems,
|
||||
]);
|
||||
|
|
|
@ -34,7 +34,7 @@ if (isLeaf($panelSettings)) {
|
|||
h($panelName)
|
||||
);
|
||||
if (!empty($panelSettings['_description'])) {
|
||||
$panelHTML .= $this->Bootstrap->genNode('div', [
|
||||
$panelHTML .= $this->Bootstrap->node('div', [
|
||||
'class' => ['mb-1',],
|
||||
], h($panelSettings['_description']));
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ if (isLeaf($panelSettings)) {
|
|||
}
|
||||
}
|
||||
}
|
||||
$panelHTML = $this->Bootstrap->genNode('div', [
|
||||
$panelHTML = $this->Bootstrap->node('div', [
|
||||
'class' => [
|
||||
'shadow',
|
||||
'p-2',
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
$(document).ready(function() {
|
||||
$("#search-settings").select2({
|
||||
data: selectData,
|
||||
placeholder: '<?= __('Search setting by typing here...') ?>',
|
||||
placeholder: '<?= __('Search a setting by typing here...') ?>',
|
||||
templateResult: formatSettingSearchResult,
|
||||
templateSelection: formatSettingSearchSelection,
|
||||
matcher: settingMatcher,
|
||||
|
|
|
@ -5,19 +5,17 @@ $table = $this->Bootstrap->table([
|
|||
'hover' => false,
|
||||
], [
|
||||
'fields' => [
|
||||
['key' => 'label', 'label' => __('Label')],
|
||||
['key' => 'name', 'label' => __('Name')],
|
||||
['key' => 'url', 'label' => __('URL'), 'formatter' => function ($value, $row) {
|
||||
['path' => 'label', 'label' => __('Label')],
|
||||
['path' => 'name', 'label' => __('Name')],
|
||||
['path' => 'url', 'label' => __('URL'), 'formatter' => function ($value, $row) {
|
||||
return sprintf('<span class="font-monospace">%s</span>', h($value));
|
||||
}],
|
||||
['key' => 'action', 'label' => __('Action'), 'formatter' => function ($value, $row, $index) {
|
||||
['path' => 'action', 'label' => __('Action'), 'formatter' => function ($value, $row, $index) {
|
||||
return $this->Bootstrap->button([
|
||||
'icon' => 'trash',
|
||||
'variant' => 'danger',
|
||||
'size' => 'sm',
|
||||
'params' => [
|
||||
'onclick' => sprintf('deleteBookmark(window.bookmarks[%s])', $index),
|
||||
]
|
||||
'onclick' => sprintf('deleteBookmark(window.bookmarks[%s])', $index),
|
||||
]);
|
||||
}],
|
||||
],
|
||||
|
|
|
@ -22,9 +22,9 @@
|
|||
],
|
||||
[
|
||||
[
|
||||
'_open' => true,
|
||||
'open' => true,
|
||||
'header' => [
|
||||
'title' => __('Meta fields')
|
||||
'text' => __('Meta fields')
|
||||
],
|
||||
'body' => $metaTemplateString,
|
||||
],
|
||||
|
|
|
@ -15,9 +15,9 @@
|
|||
],
|
||||
[
|
||||
[
|
||||
'_open' => true,
|
||||
'open' => true,
|
||||
'header' => [
|
||||
'title' => __('Meta fields')
|
||||
'text' => __('Meta fields')
|
||||
],
|
||||
'body' => $metaTemplateString,
|
||||
],
|
||||
|
|
|
@ -82,7 +82,7 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
|
|||
}
|
||||
}
|
||||
}
|
||||
$fieldContainer = $this->Bootstrap->genNode('div', [
|
||||
$fieldContainer = $this->Bootstrap->node('div', [
|
||||
'class' => [],
|
||||
], $fieldsHtml);
|
||||
echo $fieldContainer;
|
|
@ -0,0 +1,171 @@
|
|||
<?php
|
||||
$seed = 's-' . mt_rand();
|
||||
$status = $this->Hash->extract($row, $field['data_path']);
|
||||
$displayField = $this->Hash->get($row, $field['display_field_data_path']);
|
||||
|
||||
if ($status['local'] && $status['up_to_date']) {
|
||||
$variant = 'success';
|
||||
$text = __('Ok');
|
||||
} else if ($status['local'] && !$status['up_to_date']) {
|
||||
$variant = 'warning';
|
||||
$text = __('Outdated');
|
||||
} else {
|
||||
$variant = 'danger';
|
||||
$text = __('N/A');
|
||||
}
|
||||
|
||||
echo $this->Bootstrap->badge([
|
||||
'id' => $seed,
|
||||
'variant' => $variant,
|
||||
'text' => $text,
|
||||
'icon' => ($status['local'] && !$status['up_to_date']) ? 'question-circle' : false,
|
||||
'title' => $status['title'],
|
||||
'class' => [
|
||||
(($status['local'] && !$status['up_to_date']) ? 'cursor-pointer' : ''),
|
||||
],
|
||||
]);
|
||||
?>
|
||||
|
||||
<?php if ($status['local'] && !$status['up_to_date']) : ?>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
|
||||
function genTable(status) {
|
||||
status.forEach(function(row, i) {
|
||||
status[i][1] = buildTableEntry(status[i][1])
|
||||
status[i][2] = buildTableEntry(status[i][2])
|
||||
});
|
||||
const $table = HtmlHelper.table(
|
||||
['<?= __('Field name') ?>', '<?= __('Local value') ?>', '<?= __('Remote value') ?>'],
|
||||
status, {
|
||||
small: true,
|
||||
caption: `${status.length} fields`,
|
||||
}
|
||||
)
|
||||
|
||||
const $container = $('<div>')
|
||||
const $header = $('<h4>').text('<?= __('Main fields') ?>')
|
||||
$container.append($header, $table)
|
||||
return $container[0].outerHTML
|
||||
}
|
||||
|
||||
function genTableForMetafields(status) {
|
||||
let rearrangedStatus = []
|
||||
for (const [field, metafieldData] of Object.entries(status)) {
|
||||
rearrangedChanges = []
|
||||
const metaTemplate = metafieldData['meta_template']
|
||||
const metafields = metafieldData['delta']
|
||||
metafields.forEach(function(metaFields, i) {
|
||||
const localMetafield = metaFields.local
|
||||
const remoteMetafield = metaFields.remote
|
||||
rearrangedChanges.push([
|
||||
buildTableEntryForMetaField(localMetafield),
|
||||
buildTableEntryForMetaField(remoteMetafield),
|
||||
])
|
||||
})
|
||||
const $changesTable = HtmlHelper.table(
|
||||
null,
|
||||
rearrangedChanges, {
|
||||
small: true,
|
||||
borderless: true,
|
||||
striped: true,
|
||||
fixed_layout: true,
|
||||
tableClass: 'mb-0',
|
||||
}
|
||||
)
|
||||
const $field = $('<td>')
|
||||
.css('min-width', '8em')
|
||||
.text(field)
|
||||
const $template = $('<td>')
|
||||
.css('min-width', '6em')
|
||||
.append(
|
||||
$('<span>').text(metaTemplate.name),
|
||||
$('<sup>').text(`v${metaTemplate.version}`),
|
||||
)
|
||||
rearrangedStatus.push([
|
||||
$template,
|
||||
$field,
|
||||
$('<td>').attr('colspan', 2).append($changesTable),
|
||||
])
|
||||
}
|
||||
const $container = $('<div>')
|
||||
const $header = $('<h4>').text('<?= __('Meta Fields') ?>')
|
||||
const metafieldAmount = Object.values(status).reduce(function(carry, metaFields) {
|
||||
return carry + metaFields.length
|
||||
}, 0)
|
||||
const $table = HtmlHelper.table(
|
||||
['<?= __('Template') ?>', '<?= __('Field name') ?>', '<?= __('Local value') ?>', '<?= __('Remote value') ?>'],
|
||||
// ['<?= __('Field name') ?>', '<?= __('Local value') ?>', '<?= __('Remote value') ?>'],
|
||||
rearrangedStatus, {
|
||||
small: true,
|
||||
caption: `${metafieldAmount} meta-fields`,
|
||||
}
|
||||
)
|
||||
$container.append($header, $table)
|
||||
return $container[0].outerHTML
|
||||
}
|
||||
|
||||
function buildTableEntry(value) {
|
||||
let $elem
|
||||
if (typeof value === 'object') {
|
||||
$elem = $('<span>')
|
||||
.html(syntaxHighlightJson(value, 2))
|
||||
} else {
|
||||
$elem = $('<pre>')
|
||||
.text(value)
|
||||
}
|
||||
return $elem
|
||||
}
|
||||
|
||||
function buildTableEntryForMetaField(metafieldDifferences) {
|
||||
if (metafieldDifferences !== null) {
|
||||
const $container = $('<table>').addClass('table table-borderless table-xs mb-0')
|
||||
for (const [field, value] of Object.entries(metafieldDifferences)) {
|
||||
const $entry = $('<tr>')
|
||||
.append(
|
||||
$('<th>')
|
||||
.addClass('fw-normal')
|
||||
.text(field),
|
||||
$('<td>')
|
||||
.append(
|
||||
$('<pre>')
|
||||
.addClass('d-inline mb-0')
|
||||
.text(value)
|
||||
)
|
||||
)
|
||||
$container.append($entry)
|
||||
}
|
||||
return $container
|
||||
}
|
||||
return $('<span>').html(syntaxHighlightJson(metafieldDifferences))
|
||||
}
|
||||
|
||||
const status = <?= json_encode($status) ?>;
|
||||
$('#<?= $seed ?>')
|
||||
.data('sync-status', status)
|
||||
.click(function() {
|
||||
const syncStatusData = $(this).data('sync-status')['data']
|
||||
console.log(syncStatusData);
|
||||
let rearrangedStatusData = []
|
||||
for (const [field, values] of Object.entries(syncStatusData)) {
|
||||
if (field !== 'meta_fields') {
|
||||
rearrangedStatusData.push([
|
||||
field,
|
||||
values.local,
|
||||
values.remote,
|
||||
])
|
||||
}
|
||||
}
|
||||
const bodyHtml = genTable(rearrangedStatusData) +
|
||||
(syncStatusData['meta_fields'] ? genTableForMetafields(syncStatusData['meta_fields']) : '')
|
||||
const options = {
|
||||
title: '<?= __('Difference with the remote for `{0}`', $displayField) ?>',
|
||||
bodyHtml: bodyHtml,
|
||||
type: 'ok-only',
|
||||
size: 'xl',
|
||||
}
|
||||
UI.modal(options)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<?php endif; ?>
|
|
@ -41,7 +41,7 @@ foreach ($statistics['usage'] as $scope => $graphData) {
|
|||
'nodeType' => 'a',
|
||||
'onclick' => '',
|
||||
'class' => ['btn-statistics-pie-configurator-' . $seedPiechart],
|
||||
'params' => [
|
||||
'attrs' => [
|
||||
'data-bs-toggle' => 'popover',
|
||||
]
|
||||
])
|
||||
|
@ -52,7 +52,7 @@ foreach ($statistics['usage'] as $scope => $graphData) {
|
|||
$pieChart
|
||||
);
|
||||
$statPie = $this->Bootstrap->card([
|
||||
'variant' => 'secondary',
|
||||
'bodyVariant' => 'secondary',
|
||||
'bodyHTML' => $panelHtml,
|
||||
'bodyClass' => 'py-1 px-2',
|
||||
'class' => ['shadow-sm', 'h-100']
|
||||
|
|
|
@ -38,7 +38,7 @@ $panelControlHtml = sprintf(
|
|||
'nodeType' => 'a',
|
||||
'onclick' => '',
|
||||
'class' => ['btn-statistics-days-configurator-' . $seed,],
|
||||
'params' => [
|
||||
'attrs' => [
|
||||
'data-bs-toggle' => 'popover',
|
||||
]
|
||||
])
|
||||
|
@ -46,13 +46,13 @@ $panelControlHtml = sprintf(
|
|||
$createdNumber = empty($timeline['created']) ? '' : sprintf(
|
||||
'<div class="lh-1 d-flex align-items-center" title="%s">%s<span class="ms-1"> %s</span></div>',
|
||||
__('{0} Created', $timeline['created']['variation']),
|
||||
$this->Bootstrap->icon('plus', ['class' => ['fa-fw'], 'params' => ['style' => 'font-size: 60%;']]),
|
||||
$this->Bootstrap->icon('plus', ['class' => ['fa-fw'], 'attrs' => ['style' => 'font-size: 60%;']]),
|
||||
$timeline['created']['variation']
|
||||
);
|
||||
$modifiedNumber = empty($timeline['modified']) ? '' : sprintf(
|
||||
'<div class="lh-1 d-flex align-items-center" title="%s">%s<span class="ms-1"> %s</span></div>',
|
||||
__('{0} Modified', $timeline['modified']['variation']),
|
||||
$this->Bootstrap->icon('edit', ['class' => ['fa-fw'], 'params' => ['style' => 'font-size: 60%;']]),
|
||||
$this->Bootstrap->icon('edit', ['class' => ['fa-fw'], 'attrs' => ['style' => 'font-size: 60%;']]),
|
||||
$timeline['modified']['variation']
|
||||
);
|
||||
$activityNumbers = sprintf('<div class="my-1 fs-5">%s%s</div>', $createdNumber, $modifiedNumber);
|
||||
|
@ -87,7 +87,7 @@ $cardContent = sprintf(
|
|||
);
|
||||
|
||||
$card = $this->Bootstrap->card([
|
||||
'variant' => 'secondary',
|
||||
'bodyVariant' => 'secondary',
|
||||
'bodyHTML' => $cardContent,
|
||||
'bodyClass' => 'py-1 px-2',
|
||||
'class' => ['shadow-sm', 'h-100']
|
||||
|
|
|
@ -2,23 +2,24 @@
|
|||
|
||||
use Cake\Utility\Text;
|
||||
/*
|
||||
* echo $this->element('/genericElements/IndexTable/index_table', [
|
||||
* 'top_bar' => (
|
||||
* // search/filter bar information compliant with ListTopBar
|
||||
* ),
|
||||
* 'data' => [
|
||||
// the actual data to be used
|
||||
* ),
|
||||
* 'fields' => [
|
||||
* // field list with information for the paginator, the elements used for the individual cells, etc
|
||||
* ),
|
||||
* 'title' => optional title,
|
||||
* 'description' => optional description,
|
||||
* 'index_statistics' => optional statistics to be displayed for the index,
|
||||
* 'primary_id_path' => path to each primary ID (extracted and passed as $primary to fields)
|
||||
* ));
|
||||
*
|
||||
*/
|
||||
* echo $this->element('/genericElements/IndexTable/index_table', [
|
||||
* 'top_bar' => (
|
||||
* // search/filter bar information compliant with ListTopBar
|
||||
* ),
|
||||
* 'data' => [
|
||||
// the actual data to be used
|
||||
* ),
|
||||
* 'fields' => [
|
||||
* // field list with information for the paginator, the elements used for the individual cells, etc
|
||||
* ),
|
||||
* 'title' => optional title,
|
||||
* 'description' => optional description,
|
||||
* 'notice' => optional alert to be placed at the top,
|
||||
* 'index_statistics' => optional statistics to be displayed for the index,
|
||||
* 'primary_id_path' => path to each primary ID (extracted and passed as $primary to fields)
|
||||
* ));
|
||||
*
|
||||
*/
|
||||
|
||||
$newMetaFields = [];
|
||||
if (!empty($requestedMetaFields)) { // Create mapping for new index table fields on the fly
|
||||
|
@ -39,49 +40,56 @@ if (!empty($requestedMetaFields)) { // Create mapping for new index table fields
|
|||
$data['fields'] = array_merge($data['fields'], $newMetaFields);
|
||||
|
||||
$tableRandomValue = Cake\Utility\Security::randomString(8);
|
||||
echo '<div id="table-container-' . h($tableRandomValue) . '">';
|
||||
$html = '<div id="table-container-' . h($tableRandomValue) . '">';
|
||||
if (!empty($data['title'])) {
|
||||
echo Text::insert(
|
||||
'<h2 class="fw-light">:title :help</h2>',
|
||||
[
|
||||
'title' => $this->ValueGetter->get($data['title']),
|
||||
'help' => $this->Bootstrap->icon('info', [
|
||||
'class' => ['fs-6', 'align-text-top',],
|
||||
'title' => empty($data['description']) ? '' : h($data['description']),
|
||||
'params' => [
|
||||
'data-bs-toggle' => 'tooltip',
|
||||
]
|
||||
]),
|
||||
]
|
||||
);
|
||||
if (empty($embedInModal)) {
|
||||
$html .= Text::insert(
|
||||
'<h2 class="fw-light">:title :help</h2>',
|
||||
[
|
||||
'title' => h($this->ValueGetter->get($data['title'])),
|
||||
'help' => $this->Bootstrap->icon('info', [
|
||||
'class' => ['fs-6', 'align-text-top',],
|
||||
'title' => empty($data['description']) ? '' : h($data['description']),
|
||||
'attrs' => [
|
||||
'data-bs-toggle' => 'tooltip',
|
||||
]
|
||||
]),
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$pageTitle = $this->Bootstrap->node('h5', [], h($this->ValueGetter->get($data['title'])));
|
||||
}
|
||||
}
|
||||
|
||||
if(!empty($notice)) {
|
||||
echo $this->Bootstrap->alert($notice);
|
||||
$html .= $this->Bootstrap->alert($notice);
|
||||
}
|
||||
|
||||
if (!empty($modelStatistics)) {
|
||||
echo $this->element('genericElements/IndexTable/Statistics/index_statistic_scaffold', [
|
||||
$html .= $this->element('genericElements/IndexTable/Statistics/index_statistic_scaffold', [
|
||||
'statistics' => $modelStatistics,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
echo '<div class="panel">';
|
||||
$html .= '<div class="panel">';
|
||||
if (!empty($data['html'])) {
|
||||
echo sprintf('<div>%s</div>', $data['html']);
|
||||
$html .= sprintf('<div>%s</div>', $data['html']);
|
||||
}
|
||||
$skipPagination = isset($data['skip_pagination']) ? $data['skip_pagination'] : 0;
|
||||
if (!$skipPagination) {
|
||||
$paginationData = !empty($data['paginatorOptions']) ? $data['paginatorOptions'] : [];
|
||||
echo $this->element(
|
||||
if (!empty($embedInModal)) {
|
||||
$paginationData['update'] = ".modal-main-{$tableRandomValue}";
|
||||
}
|
||||
$html .= $this->element(
|
||||
'/genericElements/IndexTable/pagination',
|
||||
[
|
||||
'paginationOptions' => $paginationData,
|
||||
'tableRandomValue' => $tableRandomValue
|
||||
]
|
||||
);
|
||||
echo $this->element(
|
||||
$html .= $this->element(
|
||||
'/genericElements/IndexTable/pagination_links'
|
||||
);
|
||||
}
|
||||
|
@ -94,8 +102,8 @@ if (!empty($multiSelectData)) {
|
|||
];
|
||||
array_unshift($data['fields'], $multiSelectField);
|
||||
}
|
||||
if (!empty($data['top_bar'])) {
|
||||
echo $this->element(
|
||||
if (!empty($data['top_bar']) && empty($skipTableToolbar)) {
|
||||
$html .= $this->element(
|
||||
'/genericElements/ListTopBar/scaffold',
|
||||
[
|
||||
'data' => $data['top_bar'],
|
||||
|
@ -143,7 +151,7 @@ foreach ($data['data'] as $k => $data_row) {
|
|||
);
|
||||
}
|
||||
$tbody = '<tbody>' . $rows . '</tbody>';
|
||||
echo sprintf(
|
||||
$html .= sprintf(
|
||||
'<table class="table table-hover" id="index-table-%s" data-table-random-value="%s" data-reload-url="%s">%s%s</table>',
|
||||
$tableRandomValue,
|
||||
$tableRandomValue,
|
||||
|
@ -160,11 +168,23 @@ echo sprintf(
|
|||
$tbody
|
||||
);
|
||||
if (!$skipPagination) {
|
||||
echo $this->element('/genericElements/IndexTable/pagination_counter', $paginationData);
|
||||
echo $this->element('/genericElements/IndexTable/pagination_links');
|
||||
$html .= $this->element('/genericElements/IndexTable/pagination_counter', $paginationData);
|
||||
$html .= $this->element('/genericElements/IndexTable/pagination_links');
|
||||
}
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
if (!empty($embedInModal)) {
|
||||
echo $this->Bootstrap->modal([
|
||||
'titleHtml' => $pageTitle ?? '',
|
||||
'bodyHtml' => $html,
|
||||
'size' => 'xl',
|
||||
'type' => 'ok-only',
|
||||
'modalClass' => "modal-main-{$tableRandomValue}"
|
||||
]);
|
||||
} else {
|
||||
echo $html;
|
||||
}
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
}
|
||||
$onClick = sprintf(
|
||||
'onClick="executePagination(%s, %s);"',
|
||||
"'" . h($tableRandomValue) . "'",
|
||||
"'" . h($options['update']) . "'",
|
||||
"'{{url}}'"
|
||||
|
||||
);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
'text' => $child['text'],
|
||||
'outline' => !empty($child['outline']),
|
||||
'icon' => $child['icon'] ?? null,
|
||||
'params' => array_merge([
|
||||
'attrs' => array_merge([
|
||||
'data-onclick-function' => $child['onclick'] ?? '',
|
||||
'data-table-random-value' => $tableRandomValue,
|
||||
'onclick' => 'multiActionClickHandler(this)'
|
||||
|
|
|
@ -31,10 +31,8 @@
|
|||
$buttonConfig = [
|
||||
'icon' => 'filter',
|
||||
'variant' => $numberActiveFilters > 0 ? 'warning' : 'primary',
|
||||
'params' => [
|
||||
'title' => __('Filter index'),
|
||||
'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue))
|
||||
]
|
||||
'title' => __('Filter index'),
|
||||
'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue))
|
||||
];
|
||||
if (count($activeFilters) > 0) {
|
||||
$buttonConfig['badge'] = [
|
||||
|
|
|
@ -67,18 +67,20 @@ $numberOfElementHtml = $this->element('/genericElements/ListTopBar/group_table_a
|
|||
?>
|
||||
<?php if (!isset($data['requirement']) || $data['requirement']) : ?>
|
||||
<?php
|
||||
$now = date("Y-m-d_H-i-s");
|
||||
$downloadFilename = sprintf('%s_%s.json', $data['table_setting_id'] ?? h($model), $now);
|
||||
echo $this->Bootstrap->dropdownMenu([
|
||||
'dropdown-class' => 'ms-1',
|
||||
'alignment' => 'end',
|
||||
'direction' => 'down',
|
||||
'toggle-button' => [
|
||||
'button' => [
|
||||
'icon' => 'sliders-h',
|
||||
'variant' => 'primary',
|
||||
'class' => ['table_setting_dropdown_button'],
|
||||
],
|
||||
'submenu_alignment' => 'end',
|
||||
'submenu_direction' => 'start',
|
||||
'params' => [
|
||||
'attrs' => [
|
||||
'data-table-random-value' => $tableRandomValue,
|
||||
'data-table_setting_id' => $data['table_setting_id'],
|
||||
],
|
||||
|
@ -89,6 +91,13 @@ $numberOfElementHtml = $this->element('/genericElements/ListTopBar/group_table_a
|
|||
'keepOpen' => true,
|
||||
'menu' => $indexColumnMenu,
|
||||
],
|
||||
[
|
||||
'text' => __('Download'),
|
||||
'icon' => 'download',
|
||||
'attrs' => [
|
||||
'onclick' => sprintf('downloadIndexTable(this, "%s")', $downloadFilename),
|
||||
],
|
||||
],
|
||||
[
|
||||
'html' => $compactDisplayHtml,
|
||||
],
|
||||
|
|
|
@ -28,7 +28,7 @@ foreach ($table_data['fields'] as $field) {
|
|||
);
|
||||
}
|
||||
|
||||
$availableColumnsHtml = $this->Bootstrap->genNode('form', [
|
||||
$availableColumnsHtml = $this->Bootstrap->node('form', [
|
||||
'class' => ['visible-column-form', 'px-2 py-1'],
|
||||
], $availableColumnsHtml);
|
||||
echo $availableColumnsHtml;
|
||||
|
|
|
@ -26,7 +26,7 @@ if (!empty($meta_template)) {
|
|||
}
|
||||
}
|
||||
|
||||
$availableMetaColumnsHtml = $this->Bootstrap->genNode('form', [
|
||||
$availableMetaColumnsHtml = $this->Bootstrap->node('form', [
|
||||
'class' => ['visible-meta-column-form', 'px-2 py-1'],
|
||||
], $availableMetaColumnsHtml);
|
||||
echo $availableMetaColumnsHtml;
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
<?php
|
||||
use \Cake\Routing\Router;
|
||||
use \Cake\Utility\Hash;
|
||||
|
||||
$tabData = [
|
||||
'navs' => [],
|
||||
'content' => []
|
||||
];
|
||||
$viewElementCandidatePath = '/genericElements/SingleViews/Fields/';
|
||||
foreach($data['MetaTemplates'] as $metaTemplate) {
|
||||
if (!empty($metaTemplate->meta_template_fields)) {
|
||||
$tabData['navs'][] = [
|
||||
|
@ -15,9 +17,17 @@ foreach($data['MetaTemplates'] as $metaTemplate) {
|
|||
$labelPrintedOnce = false;
|
||||
if (!empty($metaTemplateField->metaFields)) {
|
||||
foreach ($metaTemplateField->metaFields as $metaField) {
|
||||
$viewElementCandidate = $metaTemplateField->index_type == 'text' ? 'generic' : $metaTemplateField->index_type; // Currently, single-view generic fields are not using index-view fields
|
||||
$fields[] = [
|
||||
'key' => !$labelPrintedOnce ? $metaField->field : '',
|
||||
'raw' => $metaField->value,
|
||||
// Not relying on the `type` option as this table is a special case where not all values have a label
|
||||
'raw' => $this->element(sprintf('%s%sField', $viewElementCandidatePath, $viewElementCandidate), [
|
||||
'data' => $metaField,
|
||||
'field' => [
|
||||
'path' => 'value',
|
||||
]
|
||||
]),
|
||||
'rawNoEscaping' => true,
|
||||
'warning' => $metaField->warning ?? null,
|
||||
'info' => $metaField->info ?? null,
|
||||
'danger' => $metaField->danger ?? null
|
||||
|
@ -48,7 +58,7 @@ foreach($data['MetaTemplates'] as $metaTemplate) {
|
|||
'text' => __('Migrate to version {0}', $metaTemplate['hasNewerVersion']->version),
|
||||
'variant' => 'success',
|
||||
'nodeType' => 'a',
|
||||
'params' => [
|
||||
'attrs' => [
|
||||
'href' => Router::url([
|
||||
'controller' => 'metaTemplates',
|
||||
'action' => 'migrateOldMetaTemplateToNewestVersionForEntity',
|
||||
|
|
|
@ -79,10 +79,15 @@ if (!empty($breadcrumb)) {
|
|||
if (!empty($actionEntry['badge'])) {
|
||||
$badgeNumber += 1;
|
||||
}
|
||||
if (!empty($actionEntry['isPOST'])) {
|
||||
$onclickFunction = sprintf('UI.overlayUntilResolve(this, UI.submissionModalAutoGuess(\'%s\'))', h(Router::url($actionEntry['url'])));
|
||||
} else {
|
||||
$onclickFunction = sprintf('UI.overlayUntilResolve(this, UI.modalFromUrl(\'%s\'))', h(Router::url($actionEntry['url'])));
|
||||
}
|
||||
$breadcrumbAction .= sprintf(
|
||||
'<a class="dropdown-item %s" href="#" onclick="%s"><i class="me-1 %s"></i>%s%s</a>',
|
||||
!empty($actionEntry['variant']) ? sprintf('dropdown-item-%s', $actionEntry['variant']) : '',
|
||||
sprintf('UI.overlayUntilResolve(this, UI.submissionModalAutoGuess(\'%s\'))', h(Router::url($actionEntry['url']))),
|
||||
$onclickFunction,
|
||||
!empty($actionEntry['icon']) ? $this->FontAwesome->getClass(h($actionEntry['icon'])) : '',
|
||||
h($actionEntry['label']),
|
||||
!empty($actionEntry['badge']) ? $this->Bootstrap->badge($actionEntry['badge']) : ''
|
||||
|
|
|
@ -25,6 +25,7 @@ $variant = array_flip($severity)[$maxSeverity];
|
|||
if ($hasNotification) {
|
||||
echo $this->Bootstrap->notificationBubble([
|
||||
'variant' => $variant,
|
||||
'borderVariant' => 'light',
|
||||
]);
|
||||
}
|
||||
?>
|
||||
|
|
|
@ -20,14 +20,14 @@ use Cake\Routing\Router;
|
|||
</h6>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="<?= Router::url(['controller' => 'users', 'action' => 'view', 'plugin' => null, h($this->request->getAttribute('identity')['id'])]) ?>">
|
||||
<i class="me-1 <?= $this->FontAwesome->getClass('user-circle') ?>"></i>
|
||||
<i class="me-1 fa-fw <?= $this->FontAwesome->getClass('user-circle') ?>"></i>
|
||||
<?= __('My Account') ?>
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="<?= Router::url(['controller' => 'users', 'action' => 'settings', 'plugin' => null, h($this->request->getAttribute('identity')['id'])]) ?>"
|
||||
>
|
||||
<i class="me-1 <?= $this->FontAwesome->getClass('user-cog') ?>"></i>
|
||||
<i class="me-1 fa-fw <?= $this->FontAwesome->getClass('user-cog') ?>"></i>
|
||||
<?= __('Account Settings') ?>
|
||||
</a>
|
||||
<?php
|
||||
|
@ -49,16 +49,16 @@ use Cake\Routing\Router;
|
|||
); ?>"
|
||||
>
|
||||
<?php if (!empty($this->SocialProvider->getIcon($this->request->getAttribute('identity')))): ?>
|
||||
<?= $this->SocialProvider->getIcon($this->request->getAttribute('identity')) ?>
|
||||
<?= $this->SocialProvider->getIcon($this->request->getAttribute('identity'), ['me-1']) ?>
|
||||
<?php else: ?>
|
||||
<i class="me-1 <?= $this->FontAwesome->getClass('key') ?>"></i>
|
||||
<i class="me-1 fa-fw <?= $this->FontAwesome->getClass('key') ?>"></i>
|
||||
<?php endif; ?>
|
||||
<?= __('SSO Account') ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item dropdown-item-outline-danger" href="<?= Router::url(['controller' => 'users', 'action' => 'logout', 'plugin' => null]) ?>">
|
||||
<i class="me-1 <?= $this->FontAwesome->getClass('sign-out-alt') ?>"></i>
|
||||
<i class="me-1 fa-fw <?= $this->FontAwesome->getClass('sign-out-alt') ?>"></i>
|
||||
<?= __('Logout') ?>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,5 @@ echo $this->Bootstrap->button([
|
|||
'variant' => 'primary',
|
||||
'size' => 'sm',
|
||||
'class' => 'mb-1',
|
||||
'params' => [
|
||||
'id' => 'btn-add-bookmark',
|
||||
]
|
||||
'id' => 'btn-add-bookmark',
|
||||
]);
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
'size' => 'sm',
|
||||
'icon' => h($icon),
|
||||
'class' => ['mb-1', !$validURI ? 'disabled' : ''],
|
||||
'params' => [
|
||||
'attrs' => [
|
||||
'href' => $validURI ? h($url) : '#',
|
||||
]
|
||||
]);
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
if ($childHasNotification || ($hasNotification && !empty($children))) {
|
||||
echo $this->Bootstrap->notificationBubble([
|
||||
'variant' => $childHasNotification ? $childNotificationVariant : $notificationVariant,
|
||||
'borderVariant' => 'light',
|
||||
]);
|
||||
}
|
||||
?>
|
||||
|
|
|
@ -77,7 +77,7 @@ $cardContent = sprintf(
|
|||
);
|
||||
|
||||
echo $this->Bootstrap->card([
|
||||
'variant' => 'secondary',
|
||||
'bodyVariant' => 'secondary',
|
||||
'bodyHTML' => $cardContent,
|
||||
'bodyClass' => 'p-3',
|
||||
'class' => ['shadow-sm', (empty($panelNoGrow) ? 'grow-on-hover' : '')]
|
||||
|
|
|
@ -1,28 +1,39 @@
|
|||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><?= h($title) ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><?= h($question) ?></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<?= $this->Form->postLink(
|
||||
h($actionName),
|
||||
$path,
|
||||
['class' => 'btn btn-primary button-execute', 'id' => 'submitButton']
|
||||
)
|
||||
?>
|
||||
<button type="button" class="btn btn-secondary cancel-button" data-bs-dismiss="modal"><?= __('Cancel') ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
$(document).keydown(function(e) {
|
||||
if(e.which === 13 && e.ctrlKey) {
|
||||
$('.button-execute').click();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
/**
|
||||
* Supported parameters:
|
||||
* - title: The title of the modal
|
||||
* - question: The content of the modal's body.
|
||||
* - actionName: The text of the confirm button. Basically what the confirmation will do
|
||||
* - modalOptions: Additional options to be passed to the modal
|
||||
*/
|
||||
|
||||
$form = $this->element('genericElements/Form/genericForm', [
|
||||
'entity' => null,
|
||||
'ajax' => false,
|
||||
'raw' => true,
|
||||
'data' => [
|
||||
'fields' => [
|
||||
],
|
||||
'submit' => [
|
||||
'action' => $this->request->getParam('action')
|
||||
]
|
||||
]
|
||||
]);
|
||||
$formHTML = sprintf('<div class="d-none">%s</div>', $form);
|
||||
$bodyMessage = h($question ?? '');
|
||||
$bodyHTML = sprintf('%s%s', $formHTML, $bodyMessage);
|
||||
|
||||
$defaultOptions = [
|
||||
'size' => 'lg',
|
||||
'title' => isset($title) ? h($title) : __('Confirm'),
|
||||
'type' => 'confirm',
|
||||
'confirmButton' => [
|
||||
'text' => !empty($actionName) ? h($actionName) : __('Confirm'),
|
||||
'variant' => 'primary',
|
||||
],
|
||||
];
|
||||
$modalOptions = array_merge($defaultOptions, $modalOptions ?? []);
|
||||
$modalOptions['bodyHtml'] = $bodyHTML;
|
||||
|
||||
echo $this->Bootstrap->modal($modalOptions);
|
||||
?>
|
||||
|
|
|
@ -28,8 +28,11 @@ $bodyHTML = sprintf('%s%s', $formHTML, $bodyMessage);
|
|||
echo $this->Bootstrap->modal([
|
||||
'size' => 'lg',
|
||||
'title' => !empty($deletionTitle) ? $deletionTitle : __('Delete {0}', h(Cake\Utility\Inflector::singularize(Cake\Utility\Inflector::humanize($this->request->getParam('controller'))))),
|
||||
'type' => 'confirm-danger',
|
||||
'confirmText' => !empty($deletionConfirm) ? $deletionConfirm : __('Delete'),
|
||||
'type' => 'confirm',
|
||||
'confirmButton' => [
|
||||
'text' => !empty($deletionConfirm) ? $deletionConfirm : __('Delete'),
|
||||
'variant' => 'danger',
|
||||
],
|
||||
'bodyHtml' => $bodyHTML,
|
||||
]);
|
||||
?>
|
||||
|
|
|
@ -19,12 +19,12 @@ $filteringForm = $this->Bootstrap->table(
|
|||
[
|
||||
'fields' => [
|
||||
[
|
||||
'key' => 'fieldname', 'label' => __('Field'), 'formatter' => function ($field, $row) {
|
||||
'path' => 'fieldname', 'label' => __('Field'), 'formatter' => function ($field, $row) {
|
||||
return sprintf('<span class="fieldName" data-fieldname="%s">%s</span>', h($field), h($field));
|
||||
}
|
||||
],
|
||||
[
|
||||
'key' => 'operator', 'label' => __('Operator'), 'formatter' => function ($field, $row) use ($typeMap) {
|
||||
'path' => 'operator', 'label' => __('Operator'), 'formatter' => function ($field, $row) use ($typeMap) {
|
||||
$fieldName = $row['fieldname'];
|
||||
$type = $typeMap[$fieldName] ?? 'text';
|
||||
$options = [
|
||||
|
@ -41,7 +41,7 @@ $filteringForm = $this->Bootstrap->table(
|
|||
}
|
||||
],
|
||||
[
|
||||
'key' => 'value',
|
||||
'path' => 'value',
|
||||
'labelHtml' => sprintf(
|
||||
'%s %s',
|
||||
__('Value'),
|
||||
|
@ -71,23 +71,23 @@ $filteringForm = $this->Bootstrap->table(
|
|||
|
||||
$filteringMetafields = '';
|
||||
if ($metaFieldsEnabled) {
|
||||
$helpText = $this->Bootstrap->genNode('sup', [
|
||||
$helpText = $this->Bootstrap->node('sup', [
|
||||
'class' => ['ms-1 fa fa-info'],
|
||||
'title' => __('Include help'),
|
||||
'data-bs-toggle' => 'tooltip',
|
||||
]);
|
||||
$filteringMetafields = $this->Bootstrap->genNode('h5', [], __('Meta Fields') . $helpText);
|
||||
$filteringMetafields = $this->Bootstrap->node('h5', [], __('Meta Fields') . $helpText);
|
||||
$filteringMetafields .= $this->element('genericElements/IndexTable/metafield_filtering', $metaTemplates);
|
||||
}
|
||||
|
||||
$filteringTags = '';
|
||||
if ($taggingEnabled) {
|
||||
$helpText = $this->Bootstrap->genNode('sup', [
|
||||
$helpText = $this->Bootstrap->node('sup', [
|
||||
'class' => ['ms-1 fa fa-info'],
|
||||
'title' => __('Supports negation matches (with the `!` character) and LIKE matches (with the `%` character). Example: `!exportable`, `%able`'),
|
||||
'data-bs-toggle' => 'tooltip',
|
||||
]);
|
||||
$filteringTags = $this->Bootstrap->genNode('h5', [
|
||||
$filteringTags = $this->Bootstrap->node('h5', [
|
||||
'class' => 'mt-2'
|
||||
], __('Tags') . $helpText);
|
||||
$filteringTags .= $this->Tag->tags([], [
|
||||
|
@ -104,7 +104,9 @@ echo $this->Bootstrap->modal([
|
|||
'size' => !empty($metaFieldsEnabled) ? 'xl' : 'lg',
|
||||
'type' => 'confirm',
|
||||
'bodyHtml' => $modalBody,
|
||||
'confirmText' => __('Filter'),
|
||||
'confirmButton' => [
|
||||
'text' => __('Filter'),
|
||||
],
|
||||
'confirmFunction' => 'filterIndex'
|
||||
]);
|
||||
?>
|
||||
|
|
|
@ -1 +1,14 @@
|
|||
<?= $this->Form->postLink(__('Toggle'), ['action' => 'toggle', $entity->id, $fieldName], ['confirm' => __('Are you sure you want to toggle {0} of {1}?', $fieldName. $entity->id)]) ?>
|
||||
<?php
|
||||
|
||||
$form = $this->Form->postLink(__('Toggle'), ['action' => 'toggle', $entity->id, $fieldName], ['confirm' => __('Are you sure you want to toggle `{0}` of {1}?', h($fieldName), h($entity->id))]);
|
||||
$formHTML = sprintf('<div class="d-none">%s</div>', $form);
|
||||
$bodyHTML = $formHTML;
|
||||
|
||||
$modalOptions = [
|
||||
'title' => __('Are you sure you want to toggle `{0}` of {1}?', h($fieldName), h($entity->id)),
|
||||
'type' => 'confirm',
|
||||
'bodyHtml' => $bodyHTML,
|
||||
];
|
||||
|
||||
echo $this->Bootstrap->modal($modalOptions);
|
||||
?>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue