Merge branch '3.x-ui-both' into 3.x

pull/9075/head
Sami Mokaddem 2023-05-10 10:55:39 +02:00
commit 91140316e1
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
183 changed files with 112032 additions and 104697 deletions

View File

@ -74,8 +74,10 @@ echo $this->Bootstrap->modal([
'<div class="form-container">%s</div>',
$combinedForm
),
'confirmText' => __('Create user'),
'confirmFunction' => 'submitRegistration'
'confirmButton' => [
'text' => __('Create user'),
'onclick' => 'submitRegistration',
],
]);
?>
</div>

View File

@ -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',

View File

@ -10,13 +10,15 @@ echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'type' => 'simple',
'text' => __('Add tag'),
'popover_url' => '/tags/add'
'popover_url' => '/tags/add',
'button' => [
'icon' => 'plus',
]
]
]
],
[
'type' => 'context_filters',
'context_filters' => $filteringContexts
],
[
'type' => 'search',

View File

@ -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;
}

View File

@ -172,6 +172,7 @@ class AppController extends Controller
if (!$this->ParamHandler->isRest()) {
$this->set('breadcrumb', $this->Navigation->getBreadcrumb());
$this->set('notifications', $this->Notification->getNotifications());
$this->set('iconToTableMapping', $this->Navigation->getIconToTableMapping());
}
}
}

View File

@ -428,7 +428,7 @@ class ACLComponent extends Component
if ($urlParts[1] === 'open') {
return in_array($urlParts[2], Configure::read('Cerebrate.open'));
} else {
return $this->checkAccessInternal(Inflector::camelize($urlParts[1]), $urlParts[2], $soft);
return $this->checkAccessInternal(Inflector::camelize($urlParts[1]), $urlParts[2] ?? 'index', $soft);
}
}

View File

@ -9,6 +9,8 @@ use Cake\Utility\Inflector;
use Cake\Utility\Text;
use Cake\View\ViewBuilder;
use Cake\ORM\TableRegistry;
use Cake\ORM\Query;
use Cake\Database\Expression\QueryExpression;
use Cake\Routing\Router;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\NotFoundException;
@ -32,6 +34,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'] = [];
@ -45,9 +51,12 @@ 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} >=";
$optionFilters[] = "{$filter} <=";
}
$params = $this->Controller->ParamHandler->harvestParams($optionFilters);
$params = $this->fakeContextFilter($options, $params);
@ -70,13 +79,22 @@ class CRUDComponent extends Component
$query->select($options['fields']);
}
if (!empty($options['order'])) {
$query->order($options['order']);
}
if ($this->Controller->ParamHandler->isRest()) {
if ($this->metaFieldsSupported()) {
$query = $this->includeRequestedMetaFields($query);
$orderFields = array_keys($options['order']);
if ($this->_validOrderFields($orderFields)) {
$query->order($options['order']);
$this->Controller->paginate['order'] = $options['order'];
}
$data = $query->all();
}
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()) {
$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']];
@ -107,12 +125,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);
}
$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)) {
@ -138,8 +155,11 @@ class CRUDComponent extends Component
$data[$i] = $this->attachMetaTemplatesIfNeeded($row, $metaTemplates);
}
$this->Controller->set('meta_templates', $metaTemplates);
$this->Controller->set('meta_templates_enabled', array_filter($metaTemplates, function($template) {
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(
@ -180,10 +200,12 @@ 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);
}
}
public function filtering(): void
public function filtering(array $options = []): void
{
if ($this->taggingSupported()) {
$this->Controller->set('taggingEnabled', true);
@ -195,21 +217,82 @@ class CRUDComponent extends Component
$metaTemplates = $this->getMetaTemplates()->toArray();
$this->Controller->set('metaFieldsEnabled', true);
$this->Controller->set('metaTemplates', $metaTemplates);
$typeHandlers = $this->Table->getBehavior('MetaFields')->getTypeHandlers();
$typeHandlersOperators = [];
foreach ($typeHandlers as $type => $handler) {
$typeHandlersOperators[$type] = $handler::OPERATORS;
}
$this->Controller->set('typeHandlersOperators', $typeHandlersOperators);
} else {
$this->Controller->set('metaFieldsEnabled', false);
}
$filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : [];
$typeHandlers = $this->Table->getBehavior('MetaFields')->getTypeHandlers();
$typeHandlersOperators = [];
foreach ($typeHandlers as $type => $handler) {
$typeHandlersOperators[$type] = $handler::OPERATORS;
$filtersConfigRaw= !empty($this->Controller->filterFields) ? $this->Controller->filterFields : [];
$filtersConfig = [];
foreach ($filtersConfigRaw as $fieldConfig) {
if (is_array($fieldConfig)) {
$filtersConfig[$fieldConfig['name']] = $fieldConfig;
} else {
$filtersConfig[$fieldConfig] = ['name' => $fieldConfig];
}
}
$this->Controller->set('typeHandlersOperators', $typeHandlersOperators);
$this->Controller->set('filters', $filters);
$filtersName = $this->getFilterFieldsName();
$typeMap = $this->Table->getSchema()->typeMap();
$associatedtypeMap = !empty($filtersName) ? $this->_getAssociatedTypeMap() : [];
$typeMap = array_merge(
$this->Table->getSchema()->typeMap(),
$associatedtypeMap
);
$typeMap = array_filter($typeMap, function ($field) use ($filtersName) {
return in_array($field, $filtersName);
}, ARRAY_FILTER_USE_KEY);
if (!empty($options['afterFind'])) {
$overrides = $options['afterFind']($filtersConfig, $typeMap);
$filtersConfig = $overrides['filtersConfig'];
$typeMap = $overrides['typeMap'];
}
$this->Controller->set('typeMap', $typeMap);
$this->Controller->set('filters', $filtersName);
$this->Controller->set('filtersConfig', $filtersConfig);
$this->Controller->viewBuilder()->setLayout('ajax');
$this->Controller->render('/genericTemplates/filters');
}
public function getFilterFieldsName(): array
{
$filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : [];
$filters = array_map(function($item) {
return is_array($item) ? $item['name'] : $item;
}, $filters);
return $filters;
}
/**
* From the provided fields and type mapping, convert any boolean field from the type `checkhox` to the type `radio`.
*
* @param array $fieldsConfig The array of fields to transform
* @param array $typeMap The mapping from the DB to the field type
* @param boolean $includeAny Should an `Any` option be included
* @param boolean $includeBoth Shoud a `Both` option be included
* @return array
*/
public function transformBooleanFieldsIntoRadio(array $fieldsConfig, array $typeMap, bool $includeAny = true, bool $isInline = true): array
{
foreach ($typeMap as $fieldname => $type) {
if ($type == 'boolean') {
$fieldsConfig[$fieldname]['type'] = 'radio';
$fieldsConfig[$fieldname]['inline'] = $isInline;
$fieldsConfig[$fieldname]['default'] = 'any';
$fieldsConfig[$fieldname]['options'] = [];
if ($includeAny) {
$fieldsConfig[$fieldname]['options'][] = ['value' => 'any', 'text' => __('Any'),];
}
$fieldsConfig[$fieldname]['options'][] = ['value' => 1, 'text' => __('Enabled'),];
$fieldsConfig[$fieldname]['options'][] = ['value' => 0, 'text' => __('Disabled'),];
}
}
return $fieldsConfig;
}
/**
* getResponsePayload Returns the adaquate response payload based on the request context
*
@ -258,6 +341,9 @@ class CRUDComponent extends Component
if ($this->metaFieldsSupported()) {
$metaTemplates = $this->getMetaTemplates();
$data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
if (isset($params['afterFind'])) {
$data = $params['afterFind']($data, $params);
}
}
if ($this->request->is('post')) {
$patchEntityParams = [
@ -418,6 +504,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(),
]);
@ -448,6 +535,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(),
]);
@ -466,6 +554,7 @@ class CRUDComponent extends Component
}
$entity->setDirty('meta_fields', true);
$entity->_metafields_to_delete = $metaFieldsToDelete;
return ['entity' => $entity, 'metafields_to_delete' => $metaFieldsToDelete];
}
@ -514,16 +603,16 @@ class CRUDComponent extends Component
$query->where($params['conditions']);
}
$data = $query->first();
if ($this->metaFieldsSupported()) {
$metaTemplates = $this->getMetaTemplates();
$data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
}
if (isset($params['afterFind'])) {
$data = $params['afterFind']($data, $params);
}
if (empty($data)) {
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
}
if ($this->metaFieldsSupported()) {
$metaTemplates = $this->getMetaTemplates();
$data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
}
if ($this->request->is(['post', 'put'])) {
$patchEntityParams = [
'associated' => []
@ -713,6 +802,15 @@ class CRUDComponent extends Component
]);
}
protected function setRequestedEntryAmount()
{
$user = $this->Controller->ACL->getUser();
$tableSettings = IndexSetting::getTableSetting($user, $this->Table);
if (!empty($tableSettings['number_of_element'])) {
$this->Controller->paginate['limit'] = intval($tableSettings['number_of_element']);
}
}
public function view(int $id, array $params = []): void
{
if (empty($id)) {
@ -785,6 +883,9 @@ class CRUDComponent extends Component
$query->contain($params['contain']);
}
$data = $query->first();
if (isset($params['afterFind'])) {
$data = $params['afterFind']($data, $params);
}
if (empty($data)) {
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
}
@ -807,6 +908,9 @@ class CRUDComponent extends Component
$query->contain($params['contain']);
}
$data = $query->first();
if (isset($params['afterFind'])) {
$data = $params['afterFind']($data, $params);
}
if (isset($params['beforeSave'])) {
try {
$data = $params['beforeSave']($data);
@ -1095,17 +1199,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'])) {
@ -1115,10 +1211,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
@ -1174,8 +1285,9 @@ class CRUDComponent extends Component
continue;
}
$activeFilters[$filter] = $filterValue;
$filterValue = $this->convertBooleanAnyAndBothIfNeeded($filter, $filterValue);
if (is_array($filterValue)) {
$query->where([($filter . ' IN') => $filterValue]);
$query = $this->setInCondition($query, $filter, $filterValue);
} else {
$query = $this->setValueCondition($query, $filter, $filterValue);
}
@ -1261,6 +1373,27 @@ class CRUDComponent extends Component
}
}
protected function setInCondition($query, $fieldName, $values)
{
$split = explode(' ', $fieldName);
if (count($split) == 1) {
$field = $fieldName;
$operator = '=';
} else {
$field = $split[0];
$operator = $split[1];
}
if ($operator == '=') {
return $query->where(function (QueryExpression $exp, Query $q) use ($field, $values) {
return $exp->in($field, $values);
});
} else if ($operator == '!=') {
return $query->where(function (QueryExpression $exp, Query $q) use ($field, $values) {
return $exp->notIn($field, $values);
});
}
}
protected function setFilteringContext($contextFilters, $params)
{
$filteringContexts = [];
@ -1309,7 +1442,7 @@ class CRUDComponent extends Component
{
if (empty($params['filteringLabel']) && !empty($options['contextFilters']['custom'])) {
foreach ($options['contextFilters']['custom'] as $contextFilter) {
if (!empty($contextFilter['default'])) {
if (!empty($contextFilter['default']) && empty($params)) {
$params['filteringLabel'] = $contextFilter['label'];
$this->Controller->set('fakeFilteringLabel', $contextFilter['label']);
break;
@ -1370,6 +1503,17 @@ class CRUDComponent extends Component
return $prefixedConditions;
}
protected function convertBooleanAnyAndBothIfNeeded(string $filterName, $filterValue)
{
$typeMap = $this->Table->getSchema()->typeMap();
if (!empty($typeMap[$filterName]) && $typeMap[$filterName] === 'boolean') {
if ($filterValue === 'any') {
return [true, false];
}
}
return $filterValue;
}
public function taggingSupported()
{
return $this->Table->behaviors()->has('Tag');
@ -1434,7 +1578,7 @@ class CRUDComponent extends Component
);
if ($this->Controller->ParamHandler->isRest()) {
} else if ($this->Controller->ParamHandler->isAjax()) {
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', $message, $validationErrors);
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', [], $message, $validationErrors);
} else {
$this->Controller->Flash->error($message);
if (empty($params['redirect'])) {
@ -1530,8 +1674,61 @@ 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();
}
protected function _getAssociatedTypeMap(): array
{
$typeMap = [];
foreach ($this->getFilterFieldsName() as $filter) {
$exploded = explode('.', $filter);
if (count($exploded) > 1) {
$model = $exploded[0];
$subField = $exploded[1];
if ($model == $this->Table->getAlias()) {
$typeMap[$filter] = $this->Table->getSchema()->typeMap()[$subField] ?? 'text';
} else {
$association = $this->Table->associations()->get($model);
$associatedTable = $association->getTarget();
$typeMap[$filter] = $associatedTable->getSchema()->typeMap()[$subField] ?? 'text';
}
}
}
return $typeMap;
}
protected function _validOrderFields($fields): bool
{
if (!is_array($fields)) {
$fields = [$fields];
}
foreach ($fields as $field) {
$exploded = explode('.', $field);
if (count($exploded) > 1) {
$model = $exploded[0];
$subField = $exploded[1];
if ($model == $this->Table->getAlias()) {
if (empty($this->Table->getSchema()->typeMap()[$subField])) {
return false;
}
} else {
$association = $this->Table->associations()->get($model);
$associatedTable = $association->getTarget();
if (empty($associatedTable->getSchema()->typeMap()[$subField])) {
return false;
}
}
} else {
if (empty($this->Table->getSchema()->typeMap()[$field])) {
return false;
}
}
}
return true;
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class BroodsNavigation extends BaseNavigation
{
public function addLinks()
{
$this->bcf->addLink('Broods', 'view', 'LocalTools', 'broodTools');
$this->bcf->addLink('Broods', 'edit', 'LocalTools', 'broodTools');
}
}

View File

@ -1,8 +0,0 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class EncryptionKeysNavigation extends BaseNavigation
{
}

View File

@ -0,0 +1,117 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class EventsNavigation extends BaseNavigation
{
function addRoutes()
{
}
public function addParents()
{
}
public function addLinks()
{
$passedData = $this->request->getParam('pass');
$eventID = $passedData[0] ?? 0;
$this->bcf->removeAction('Events', 'view', 'Events', 'delete');
$this->bcf->addCustomLink('Events', 'view', '/logs/event_index', __('View history'), [
'url' => sprintf('/logs/event_index/%s', h($eventID)),
'icon' => 'clock-rotate-left',
]);
$this->bcf->addCustomLink('Events', 'view', '/events/viewgraph', __('Explore'), [
'url' => sprintf('/events/viewgraph/%s', h($eventID)),
'icon' => 'binoculars',
]);
}
public function addActions()
{
/* Add */
$this->bcf->addCustomAction('Events', 'view', '/link', 'Add Object', [
'menu' => 'add',
'menu_primary' => true,
'icon' => $this->bcf->iconToTableMapping['Objects'],
]);
$this->bcf->addCustomAction('Events', 'view', '/link', 'Add Attribute', [
'menu' => 'add',
'icon' => $this->bcf->iconToTableMapping['Attributes'],
]);
$this->bcf->addCustomAction('Events', 'view', '/link', 'Add Report', [
'menu' => 'add',
'icon' => $this->bcf->iconToTableMapping['EventReports'],
]);
/* Publish */
$this->bcf->addCustomAction('Events', 'view', '/link', 'Publish', [
'menu' => 'publish',
'icon' => 'paper-plane',
]);
$this->bcf->addCustomAction('Events', 'view', '/link', 'Unpublish', [
'menu' => 'publish',
'icon' => ['stacked' => [
['icon' => 'ban', 'class' => 'text-muted',],
['icon' => 'paper-plane', 'class' => 'text-body',]
]],
'variant' => 'warning',
]);
$this->bcf->addCustomAction('Events', 'view', '/link', 'Publish no email', [
'menu' => 'publish',
'icon' => ['stacked' => [
['icon' => 'ban', 'class' => 'text-muted',],
['icon' => 'envelope', 'class' => 'text-body',]
]],
]);
$this->bcf->addCustomAction('Events', 'view', '/link', 'Publish to ZMQ', [
'menu' => 'publish',
]);
/* Events Action */
$this->bcf->registerActionMenuConfig('Events', 'view', 'event-actions', [
'label' => 'Event Actions',
]);
$this->bcf->addCustomAction('Events', 'view', '/link', 'Enrich event', [
'menu' => 'event-actions',
'icon' => 'brain',
]);
$this->bcf->addCustomAction('Events', 'view', '/link', 'Extend event', [
'menu' => 'event-actions',
'icon' => 'expand',
]);
$this->bcf->addCustomAction('Events', 'view', '/link', 'Contact Org', [
'menu' => 'event-actions',
'icon' => 'comment-dots',
]);
$this->bcf->addCustomAction('Events', 'view', '/link', 'Delete Event', [
'menu' => 'event-actions',
'icon' => 'trash',
'variant' => 'danger',
]);
/* Events Action */
$this->bcf->registerActionMenuConfig('Events', 'view', 'import-export', [
'label' => 'Import/Export',
'icon' => [
'icon' => 'arrow-right-arrow-left',
'class' => 'fa-rotate-90 me-1',
],
]);
$this->bcf->addCustomAction('Events', 'view', '/link', 'Populate from', [
'menu' => 'import-export',
'icon' => 'puzzle-piece',
]);
$this->bcf->addCustomAction('Events', 'view', '/link', 'Merge from', [
'menu' => 'import-export',
'icon' => 'object-group',
]);
$this->bcf->addCustomAction('Events', 'view', '/link', 'Download as', [
'menu' => 'import-export',
'icon' => 'download',
]);
}
}

View File

@ -1,49 +0,0 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class LocalToolsNavigation extends BaseNavigation
{
function addRoutes()
{
$this->bcf->addRoute('LocalTools', 'viewConnector', [
'label' => __('View'),
'textGetter' => 'connector',
'url' => '/localTools/viewConnector/{{connector}}',
'url_vars' => ['connector' => 'connector'],
]);
$this->bcf->addRoute('LocalTools', 'broodTools', [
'label' => __('Brood Tools'),
'url' => '/localTools/broodTools/{{id}}',
'url_vars' => ['id' => 'id'],
]);
}
public function addParents()
{
$this->bcf->addParent('LocalTools', 'viewConnector', 'LocalTools', 'index');
}
public function addLinks()
{
$passedData = $this->request->getParam('pass');
if (!empty($passedData[0])) {
$brood_id = $passedData[0];
$this->bcf->addParent('LocalTools', 'broodTools', 'Broods', 'view', [
'textGetter' => [
'path' => 'name',
'varname' => 'broodEntity',
],
'url' => "/broods/view/{$brood_id}",
]);
$this->bcf->addLink('LocalTools', 'broodTools', 'Broods', 'view', [
'url' => "/broods/view/{$brood_id}",
]);
$this->bcf->addLink('LocalTools', 'broodTools', 'Broods', 'edit', [
'url' => "/broods/view/{$brood_id}",
]);
}
$this->bcf->addSelfLink('LocalTools', 'broodTools');
}
}

View File

@ -1,109 +0,0 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class MetaTemplatesNavigation extends BaseNavigation
{
function addRoutes()
{
$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'),
'icon' => 'check-square',
'url' => '/metaTemplates/enable/{{id}}/enabled',
'url_vars' => ['id' => 'id'],
]);
$this->bcf->addRoute('MetaTemplates', 'set_default', [
'label' => __('Set as default'),
'icon' => 'check-square',
'url' => '/metaTemplates/toggle/{{id}}/default',
'url_vars' => ['id' => 'id'],
]);
$totalUpdateCount = 0;
if (!empty($this->viewVars['updateableTemplates']['automatically-updateable']) && !empty($this->viewVars['updateableTemplates']['new'])) {
$udpateCount = count($this->viewVars['updateableTemplates']['automatically-updateable']) ?? 0;
$newCount = count($this->viewVars['updateableTemplates']['new']) ?? 0;
$totalUpdateCount = $udpateCount + $newCount;
}
$updateRouteConfig = [
'label' => __('Update all templates'),
'icon' => 'download',
'url' => '/metaTemplates/updateAllTemplates',
];
if ($totalUpdateCount > 0) {
$updateRouteConfig['badge'] = [
'text' => h($totalUpdateCount),
'variant' => 'warning',
'title' => __('There are {0} new meta-template(s) and {1} update(s) available', h($newCount), h($udpateCount)),
];
}
$this->bcf->addRoute('MetaTemplates', 'update_all_templates', $updateRouteConfig);
$this->bcf->addRoute('MetaTemplates', 'update', [
'label' => __('Update template'),
'icon' => 'download',
'url' => '/metaTemplates/update',
]);
$this->bcf->addRoute('MetaTemplates', 'prune_outdated_template', [
'label' => __('Prune outdated template'),
'icon' => 'trash',
'url' => '/metaTemplates/prune_outdated_template',
]);
}
public function addParents()
{
$this->bcf->addParent('MetaTemplates', 'view', 'MetaTemplates', 'index');
$this->bcf->addParent('MetaTemplates', 'update', 'MetaTemplates', 'index');
}
public function addLinks()
{
$this->bcf->addSelfLink('MetaTemplates', 'view');
}
public function addActions()
{
$totalUpdateCount = 0;
if (!empty($this->viewVars['updateableTemplates']['not-up-to-date']) || !empty($this->viewVars['updateableTemplates']['new'])) {
$udpateCount = count($this->viewVars['updateableTemplates']['not-up-to-date']) ?? 0;
$newCount = count($this->viewVars['updateableTemplates']['new']) ?? 0;
$totalUpdateCount = $udpateCount + $newCount;
}
$updateAllActionConfig = [
'label' => __('Update template'),
'url' => '/metaTemplates/updateAllTemplates',
'url_vars' => ['id' => 'id'],
];
if ($totalUpdateCount > 0) {
$updateAllActionConfig['badge'] = [
'text' => h($totalUpdateCount),
'variant' => 'warning',
'title' => __('There are {0} new meta-template(s) and {1} update(s) available', h($newCount), h($udpateCount)),
];
}
$this->bcf->addAction('MetaTemplates', 'index', 'MetaTemplates', 'update_all_templates', $updateAllActionConfig);
$this->bcf->addAction('MetaTemplates', 'index', 'MetaTemplates', 'prune_outdated_template', [
'label' => __('Prune outdated template'),
'url' => '/metaTemplates/prune_outdated_template',
]);
if (empty($this->viewVars['updateableTemplates']['up-to-date'])) {
$this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'update', [
'label' => __('Update template'),
'url' => '/metaTemplates/update/{{id}}',
'url_vars' => ['id' => 'id'],
'variant' => 'warning',
'badge' => [
'variant' => 'warning',
'title' => __('Update available')
]
]);
}
$this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'enable');
$this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'set_default');
}
}

View File

@ -1,8 +1,15 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class OrganisationsNavigation extends BaseNavigation
{
public function addActions()
{
$this->bcf->addCustomAction('Organisations', 'index', '/admin/users/email', __('Contact Organisation'), [
'icon' => 'comment-dots',
]);
}
}

View File

@ -1,43 +0,0 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class OutboxNavigation extends BaseNavigation
{
function addRoutes()
{
$this->bcf->addRoute('Outbox', 'index', $this->bcf->defaultCRUD('Outbox', 'index'));
$this->bcf->addRoute('Outbox', 'view', $this->bcf->defaultCRUD('Outbox', 'view'));
$this->bcf->addRoute('Outbox', 'discard', [
'label' => __('Discard request'),
'icon' => 'trash',
'url' => '/outbox/discard/{{id}}',
'url_vars' => ['id' => 'id'],
]);
$this->bcf->addRoute('Outbox', 'process', [
'label' => __('Process request'),
'icon' => 'cogs',
'url' => '/outbox/process/{{id}}',
'url_vars' => ['id' => 'id'],
]);
}
public function addParents()
{
$this->bcf->addParent('Outbox', 'view', 'Outbox', 'index');
$this->bcf->addParent('Outbox', 'discard', 'Outbox', 'index');
$this->bcf->addParent('Outbox', 'process', 'Outbox', 'index');
}
public function addLinks()
{
$this->bcf->addSelfLink('Outbox', 'view');
}
public function addActions()
{
$this->bcf->addAction('Outbox', 'view', 'Outbox', 'process');
$this->bcf->addAction('Outbox', 'view', 'Outbox', 'discard');
}
}

View File

@ -5,6 +5,16 @@ require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'b
class UserSettingsNavigation extends BaseNavigation
{
public function addRoutes()
{
$this->bcf->addRoute('UserSettings', 'index', [
'label' => __('User settings'),
'url' => '/user-settings/index/',
'icon' => 'user-cog'
]);
}
public function addLinks()
{
$bcf = $this->bcf;

View File

@ -8,6 +8,12 @@ class UsersNavigation extends BaseNavigation
{
public function addRoutes()
{
$this->bcf->addRoute('Users', 'registrations', [
'label' => __('Pending Registration'),
'url' => '/users/registrations',
'icon' => 'user-clock',
// 'is-go-to' => true,
]);
$this->bcf->addRoute('Users', 'settings', [
'label' => __('User settings'),
'url' => '/users/settings/',
@ -27,6 +33,8 @@ class UsersNavigation extends BaseNavigation
$passedData = $this->request->getParam('pass');
$currentUserId = empty($this->currentUserId) ? null : $this->currentUserId;
$currentUser = $this->currentUser;
$this->bcf->addLink('Users', 'index', 'UserSettings', 'index');
$this->bcf->addLink('Users', 'view', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData, $currentUser) {
if (!empty($passedData[0])) {
$user_id = $passedData[0];
@ -103,5 +111,16 @@ class UsersNavigation extends BaseNavigation
$this->bcf->addSelfLink('Users', 'settings', [
'label' => __('Account settings')
]);
$this->bcf->addLink('Users', 'index', 'Users', 'registrations', [
// 'badge' => ['text' => 123, 'variant' => 'warning']
]);
}
public function addActions()
{
$this->bcf->addCustomAction('Users', 'index', '/admin/users/email', __('Contact Users'), [
'icon' => 'comment-dots',
]);
}
}

View File

@ -15,75 +15,51 @@ class Sidemenu {
public function get(): array
{
return [
__('Threat Intel') => [
'Data' => [
'label' => __('Data'),
return [
__('Threat intelligence') => [
'Events' => [
'label' => __('List Events'),
'icon' => $this->iconTable['Events'],
'url' => '/events/index',
],
'PeriodicReport' => [
'label' => __('View Periodic Report'),
'icon' => $this->iconTable['PeriodicReport'],
'url' => '/users/viewPeriodicSummary/daily',
],
'Dashboard' => [
'label' => __('Dashboard'),
'icon' => $this->iconTable['Dashboard'],
'url' => '/dashboards',
],
'ListExport' => [
'label' => __('List & Export'),
'icon' => 'list-alt',
'children' => [
'events' => [
'url' => '/Events/index',
'label' => __('Events'),
],
'attributes' => [
'Attributes' => [
'label' => __('List Attributes'),
'icon' => $this->iconTable['Attributes'],
'url' => '/attributes/index',
'label' => __('Attributes'),
],
'objects' => [
'url' => '/objects/index',
'label' => __('Objects'),
'Proposals' => [
'label' => __('List Proposals'),
'icon' => $this->iconTable['Proposals'],
'url' => '/shadow_attributes/index/all:0',
],
]
],
'Context' => [
'label' => __('Context'),
'icon' => $this->iconTable['Context'],
'url' => '/galaxies/index',
'children' => [
'galaxies' => [
'url' => '/galaxies/index',
'label' => __('Galaxies')
'Delegation' => [
'label' => __('View Delegations'),
'icon' => $this->iconTable['Events'],
'url' => 'event_delegations/index/context:pending',
],
'taxonomies' => [
'url' => '/taxonomies/index',
'label' => __('Taxonomies')
'Export' => [
'label' => __('Export'),
'icon' => 'download',
'url' => '/events/export',
],
'tags' => [
'url' => '/tags/index',
'label' => __('Tags')
]
]
],
'Insights' => [
'label' => __('Insights'),
'icon' => $this->iconTable['Insights'],
'url' => '/dashboards/index',
'children' => [
'galaxies' => [
'url' => '/galaxies/index',
'label' => __('Galaxies')
],
'galaxy_relationships' => [
'url' => '/galaxy_cluster_relations/index',
'label' => __('Relationships')
],
'taxonomies' => [
'url' => '/taxonomies/index',
'label' => __('Taxonomies')
],
'tags' => [
'url' => '/tags/index',
'label' => __('Tags')
],
'tag_collections' => [
'url' => '/tag_collections/index',
'label' => __('Tag Collections')
]
]
],
],
],
__('Community') => [
__('Directory') => [
'Organisations' => [
'label' => __('Organisations'),
'icon' => $this->iconTable['Organisations'],
@ -92,87 +68,377 @@ class Sidemenu {
'SharingGroups' => [
'label' => __('Sharing Groups'),
'icon' => $this->iconTable['SharingGroups'],
'url' => '/sharingGroups/index',
'url' => '/sharing_groups/index',
],
],
__('Connectivity') => [
'Connectivity' => [
'label' => __('Connectivity'),
'icon' => $this->iconTable['Connectivity'],
__('Knowledge Base') => [
'Tags' => [
'label' => __('Tags'),
'icon' => $this->iconTable['Tags'],
'url' => '/tags/index',
],
'Taxonomies' => [
'label' => __('Taxonomies'),
'icon' => $this->iconTable['Taxonomies'],
'url' => '/taxonomies/index',
],
'Galaxies' => [
'label' => __('Galaxies'),
'icon' => $this->iconTable['Galaxies'],
'url' => '/galaxies/index',
],
'Templates' => [
'label' => __('Templates'),
'icon' => 'ruler',
'children' => [
'servers' => [
'url' => '/servers/index',
'label' => __('Servers'),
'ObjectTemplates' => [
'label' => __('Object Templates'),
'icon' => $this->iconTable['ObjectTemplates'],
'url' => '/objectTemplates/index',
],
'feeds' => [
'url' => '/feeds/index',
'label' => __('Feeds'),
'TagCollections' => [
'label' => __('Tag Collections'),
'icon' => $this->iconTable['TagCollections'],
'url' => '/tag_collections/index',
],
'Templates' => [
'label' => __('List Templates'),
'icon' => $this->iconTable['Templates'],
'url' => '/templates/index',
],
],
],
],
__('Behaviors') => [
'Warninglists' => [
'label' => __('Warninglists'),
'icon' => $this->iconTable['Warninglists'],
'url' => '/warninglists/index',
],
'Workflows' => [
'label' => __('Workflows'),
'icon' => $this->iconTable['Workflows'],
'url' => '/workflows/index',
],
'Input/Output Filters' => [
'label' => __('Input Filters'),
'icon' => 'filter',
'children' => [
'CorrelationsExclusions' => [
'label' => __('Correlation Exclusions'),
'icon' => $this->iconTable['CorrelationsExclusions'],
'url' => '/correlation_exclusions/index',
],
'DecayingModels' => [
'label' => __('Decaying Models'),
'icon' => $this->iconTable['DecayingModels'],
'url' => '/decayingModel/index',
],
'ImportRegexp' => [
'label' => __('Import Regexp'),
'icon' => $this->iconTable['ImportRegexp'],
'url' => '/admin/regexp/index',
],
'SignatureAllowedlists' => [
'label' => __('Signature Allowedlists'),
'icon' => $this->iconTable['SignatureAllowedlists'],
'url' => '/admin/allowedlists/index',
],
'NoticeLists' => [
'label' => __('NoticeLists'),
'icon' => $this->iconTable['NoticeLists'],
'url' => '/noticelists/index',
],
'cerebrates' => [
'url' => '/cerebrates/index',
'label' => __('Cerebrates'),
]
]
]
],
'Correlations' => [
'label' => __('Correlations'),
'icon' => $this->iconTable['Correlations'],
'url' => '/correlation_exclusions/index',
],
],
__('Synchronisation') => [
'Servers' => [
'label' => __('List Servers'),
'icon' => $this->iconTable['Servers'],
'url' => '/servers/index',
],
'Feeds' => [
'label' => __('List Feeds'),
'icon' => $this->iconTable['Feeds'],
'url' => '/feeds/index',
],
'Communities' => [
'label' => __('Communities'),
'icon' => 'handshake',
'children' => [
'Communities' => [
'label' => __('Communities'),
'icon' => $this->iconTable['Communities'],
'url' => '/communities/index',
],
'Cerebrate' => [
'label' => __('Cerebrate'),
'icon' => $this->iconTable['Cerebrate'],
'url' => '/cerebrates/index',
],
'TaxiiServers' => [
'label' => __('Taxii Servers'),
'icon' => $this->iconTable['TaxiiServers'],
'url' => '/TaxiiServers/index',
],
],
],
],
__('Administration') => [
'Users' => [
'label' => __('Users'),
'icon' => $this->iconTable['Users'],
'url' => '/admin/users/index',
],
'Roles' => [
'label' => __('Roles'),
'icon' => $this->iconTable['Roles'],
'url' => '/roles/index',
],
'Users' => [
'label' => __('Users'),
'icon' => $this->iconTable['Users'],
'url' => '/users/index',
'ServerSettings' => [
'label' => __('Settings & Maintenance'),
'icon' => $this->iconTable['ServerSettings'],
'url' => '/servers/serverSettings',
],
'UserSettings' => [
'label' => __('Users Settings'),
'icon' => $this->iconTable['UserSettings'],
'url' => '/user-settings/index',
'Jobs' => [
'label' => __('Jobs'),
'icon' => $this->iconTable['Jobs'],
'url' => '/jobs/index',
],
'Messages' => [
'label' => __('Messages'),
'icon' => $this->iconTable['Inbox'],
'url' => '/inbox/index',
'BlockRules' => [
'label' => __('Block Rules'),
'icon' => $this->iconTable['BlockRules'],
'children' => [
'inbox' => [
'url' => '/inbox/index',
'label' => __('Inbox'),
'EventsBlockRules' => [
'label' => __('Events Block Rules'),
'icon' => $this->iconTable['EventsBlockRules'],
'url' => '/eventBlocklists',
],
'outbox' => [
'url' => '/outbox/index',
'label' => __('Outbox'),
'OrganisationsRules' => [
'label' => __('Organisations Rules'),
'icon' => $this->iconTable['OrganisationsRules'],
'url' => '/orgBlocklists',
],
],
],
'Logs' => [
'label' => __('Logs'),
'icon' => $this->iconTable['Logs'],
'children' => [
'ApplicationLogs' => [
'label' => __('Application Logs'),
'icon' => $this->iconTable['ApplicationLogs'],
'url' => '/logs/index',
],
'AccessLogs' => [
'label' => __('Access Logs'),
'icon' => $this->iconTable['AccessLogs'],
'url' => '/admin/access_logs/index',
],
]
],
'Instance' => [
'label' => __('Instance'),
'icon' => $this->iconTable['Instance'],
'children' => [
'Settings' => [
'label' => __('Settings'),
'url' => '/instance/settings',
'icon' => 'cogs',
],
'Database' => [
'label' => __('Database'),
'url' => '/instance/migrationIndex',
'icon' => 'database',
],
'AuditLogs' => [
'label' => __('Audit Logs'),
'url' => '/auditLogs/index',
'icon' => 'history',
]
]
'RestClient' => [
'label' => __('REST Client'),
'icon' => $this->iconTable['RestClient'],
'url' => '/api/rest',
],
'Statistics' => [
'label' => __('Statistics'),
'icon' => 'chart-pie',
'url' => '/users/statistics',
],
],
__('Documentation') => [
'API' => [
'label' => __('API'),
'label' => __('Open API'),
'icon' => $this->iconTable['API'],
'url' => '/api/index',
'url' => '/api/openapi',
],
'UserGuide' => [
'label' => __('User Guide'),
'icon' => 'book-open',
'url' => 'https://www.circl.lu/doc/misp/',
],
'Data Model' => [
'label' => __('Data Model'),
'icon' => 'shapes',
'url' => '/pages/display/doc/categories_and_types',
],
'TermsConditions' => [
'label' => __('Terms & Conditions'),
'icon' => 'gavel',
'url' => '/users/terms',
],
]
];
];
}
#public function get(): array
#{
# return [
# __('Threat Intel') => [
# 'Data' => [
# 'label' => __('Data'),
# 'icon' => $this->iconTable['Events'],
# 'url' => '/events/index',
# 'children' => [
# 'events' => [
# 'url' => '/Events/index',
# 'label' => __('Events'),
# ],
# 'attributes' => [
# 'url' => '/attributes/index',
# 'label' => __('Attributes'),
# ],
# 'objects' => [
# 'url' => '/objects/index',
# 'label' => __('Objects'),
# ],
# ]
# ],
# 'Context' => [
# 'label' => __('Context'),
# 'icon' => $this->iconTable['Context'],
# 'url' => '/galaxies/index',
# 'children' => [
# 'galaxies' => [
# 'url' => '/galaxies/index',
# 'label' => __('Galaxies')
# ],
# 'taxonomies' => [
# 'url' => '/taxonomies/index',
# 'label' => __('Taxonomies')
# ],
# 'tags' => [
# 'url' => '/tags/index',
# 'label' => __('Tags')
# ]
# ]
# ],
# 'Insights' => [
# 'label' => __('Insights'),
# 'icon' => $this->iconTable['Insights'],
# 'url' => '/dashboards/index',
# 'children' => [
# 'galaxies' => [
# 'url' => '/galaxies/index',
# 'label' => __('Galaxies')
# ],
# 'galaxy_relationships' => [
# 'url' => '/galaxy_cluster_relations/index',
# 'label' => __('Relationships')
# ],
# 'taxonomies' => [
# 'url' => '/taxonomies/index',
# 'label' => __('Taxonomies')
# ],
# 'tags' => [
# 'url' => '/tags/index',
# 'label' => __('Tags')
# ],
# 'tag_collections' => [
# 'url' => '/tag_collections/index',
# 'label' => __('Tag Collections')
# ]
# ]
# ],
# ],
# __('Community') => [
# 'Organisations' => [
# 'label' => __('Organisations'),
# 'icon' => $this->iconTable['Organisations'],
# 'url' => '/organisations/index',
# ],
# 'SharingGroups' => [
# 'label' => __('Sharing Groups'),
# 'icon' => $this->iconTable['SharingGroups'],
# 'url' => '/sharingGroups/index',
# ],
# ],
# __('Connectivity') => [
# 'Connectivity' => [
# 'label' => __('Connectivity'),
# 'icon' => $this->iconTable['Connectivity'],
# 'children' => [
# 'servers' => [
# 'url' => '/servers/index',
# 'label' => __('Servers'),
# ],
# 'feeds' => [
# 'url' => '/feeds/index',
# 'label' => __('Feeds'),
# ],
# 'cerebrates' => [
# 'url' => '/cerebrates/index',
# 'label' => __('Cerebrates'),
# ]
# ]
# ]
# ],
# __('Administration') => [
# 'Roles' => [
# 'label' => __('Roles'),
# 'icon' => $this->iconTable['Roles'],
# 'url' => '/roles/index',
# ],
# 'Users' => [
# 'label' => __('Users'),
# 'icon' => $this->iconTable['Users'],
# 'url' => '/users/index',
# ],
# 'UserSettings' => [
# 'label' => __('Users Settings'),
# 'icon' => $this->iconTable['UserSettings'],
# 'url' => '/user-settings/index',
# ],
# 'Messages' => [
# 'label' => __('Messages'),
# 'icon' => $this->iconTable['Inbox'],
# 'url' => '/inbox/index',
# 'children' => [
# 'inbox' => [
# 'url' => '/inbox/index',
# 'label' => __('Inbox'),
# ],
# 'outbox' => [
# 'url' => '/outbox/index',
# 'label' => __('Outbox'),
# ],
# ]
# ],
# 'Instance' => [
# 'label' => __('Instance'),
# 'icon' => $this->iconTable['Instance'],
# 'children' => [
# 'Settings' => [
# 'label' => __('Settings'),
# 'url' => '/instance/settings',
# 'icon' => 'cogs',
# ],
# 'Database' => [
# 'label' => __('Database'),
# 'url' => '/instance/migrationIndex',
# 'icon' => 'database',
# ],
# 'AuditLogs' => [
# 'label' => __('Audit Logs'),
# 'url' => '/auditLogs/index',
# 'icon' => 'history',
# ]
# ]
# ],
# 'API' => [
# 'label' => __('API'),
# 'icon' => $this->iconTable['API'],
# 'url' => '/api/index',
# ],
# ]
# ];
#}
}

View File

@ -21,22 +21,63 @@ class NavigationComponent extends Component
public $breadcrumb = null;
public $fullBreadcrumb = null;
public $iconToTableMapping = [
//'Individuals' => 'address-book',
'Events' => 'envelope-open-text',
'Attributes' => 'cube',
'Objects' => 'cubes',
'EventReports' => 'file-lines',
'PeriodicReport' => 'newspaper',
'Dashboard' => 'chart-line',
'Proposals' => 'pen-square',
'Taxonomies' => 'book',
'Galaxies' => 'atlas',
'ObjectTemplates' => 'ruler-combined',
'Tags' => 'tag',
'TagCollections' => 'tags',
'Templates' => 'pencil-ruler',
'Warninglists' => ['stacked' => [
['icon' => 'file'],
['icon' => 'exclamation-triangle', 'class' => 'fa-inverse', 'style' => 'top: 0.2em;'],
]],
'Workflows' => 'sitemap',
'CorrelationsExclusions' => ['stacked' => [
['icon' => 'ban'],
['icon' => 'project-diagram', 'class' => '', 'style' => ''],
]],
'DecayingModels' => 'hourglass-end',
'ImportRegexp' => 'file-import',
'SignatureAllowedlists' => 'fingerprint',
'NoticeLists' => 'list',
'Correlations' => 'project-diagram',
'Servers' => 'network-wired',
'Communities' => 'handshake-simple',
'Cerebrate' => ['image' => '/img/cerebrate-icon-purple.png',],
'TaxiiServers' => ['image' => '/img/taxii-icon.png',],
'ServerSettings' => 'cogs',
'Jobs' => 'robot',
'BlockRules' => 'ban',
'Logs' => 'history',
'AccessLogs' => 'door-open',
'ApplicationLogs' => 'list-ul',
'OrganisationsRules' => ['stacked' => [
['icon' => 'ban', 'class' => 'text-muted',],
['icon' => 'building', 'class' => 'text-body',]
]],
'EventsBlockRules' => ['stacked' => [
['icon' => 'ban', 'class' => 'text-muted',],
['icon' => 'envelope-open-text', 'class' => 'text-body',],
]],
'SharingGroups' => 'users-rectangle',
'Organisations' => 'building',
'EncryptionKeys' => 'key',
'SharingGroups' => 'user-friends',
'Connectivity' => 'network-wired',
'Roles' => 'id-badge',
'Users' => 'users',
'Feeds' => 'rss',
'Roles' => 'id-badge',
'API' => 'code',
'UserSettings' => 'user-cog',
'Inbox' => 'inbox',
'Instance' => 'server',
'Tags' => 'tags',
'API' => 'code',
'Feeds' => 'rss',
'Events' => 'project-diagram',
'Context' => 'book-reader',
'Insights' => 'lightbulb'
'RestClient' => ['stacked' => [
['icon' => 'cloud'],
['icon' => 'cog', 'class' => 'fa-inverse']
]],
];
public function initialize(array $config): void
@ -46,8 +87,8 @@ class NavigationComponent extends Component
public function beforeRender($event)
{
$this->fullBreadcrumb = null;
//$this->fullBreadcrumb = $this->genBreadcrumb();
// $this->fullBreadcrumb = null;
$this->fullBreadcrumb = $this->genBreadcrumb();
}
public function getSideMenu(): array
@ -85,6 +126,11 @@ class NavigationComponent extends Component
return $links;
}
public function getIconToTableMapping(): array
{
return $this->iconToTableMapping;
}
public function getBreadcrumb(): array
{
$controller = $this->request->getParam('controller');
@ -156,12 +202,12 @@ class NavigationComponent extends Component
$CRUDControllers = [
//'Individuals',
'Organisations',
'EncryptionKeys',
'SharingGroups',
'Roles',
'Users',
'Tags',
'UserSettings',
'Events',
];
foreach ($CRUDControllers as $controller) {
$bcf->setDefaultCRUDForModel($controller);
@ -186,7 +232,7 @@ class NavigationComponent extends Component
class BreadcrumbFactory
{
private $endpoints = [];
private $iconToTableMapping = [];
public $iconToTableMapping = [];
public function __construct($iconToTableMapping)
{
@ -213,7 +259,7 @@ class BreadcrumbFactory
]);
} else if ($action === 'add') {
$item = $this->genRouteConfig($controller, $action, [
'label' => __('[new {0}]', $controller),
'label' => __('Create {0}', $controller),
'icon' => 'plus',
'url' => "/{$controller}/add",
]);
@ -232,6 +278,7 @@ class BreadcrumbFactory
'url' => "/{$controller}/delete/{{id}}",
'url_vars' => ['id' => 'id'],
'textGetter' => !empty($table->getDisplayField()) ? $table->getDisplayField() : 'id',
'variant' => 'danger',
]);
}
$item['route_path'] = "{$controller}:{$action}";
@ -252,6 +299,8 @@ 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, 'variant');
$routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'is-go-to');
return $routeConfig;
}
@ -267,7 +316,7 @@ class BreadcrumbFactory
return $arr;
}
public function addRoute($controller, $action, $config = []) {
public function addRoute(string $controller, string $action, array $config = []) {
$this->endpoints[$controller][$action] = $this->genRouteConfig($controller, $action, $config);
}
@ -289,7 +338,8 @@ class BreadcrumbFactory
$this->addLink($controller, 'edit', $controller, 'view');
$this->addSelfLink($controller, 'edit');
$this->addAction($controller, 'view', $controller, 'add');
// $this->addAction($controller, 'index', $controller, 'add');
// $this->addAction($controller, 'view', $controller, 'add');
$this->addAction($controller, 'view', $controller, 'delete');
$this->addAction($controller, 'edit', $controller, 'add');
$this->addAction($controller, 'edit', $controller, 'delete');
@ -312,7 +362,7 @@ class BreadcrumbFactory
{
$routeSourceConfig = $this->get($sourceController, $sourceAction);
$routeTargetConfig = $this->get($targetController, $targetAction);
$overrides = $this->execClosureIfNeeded($overrides, $routeSourceConfig);
$overrides = $this->execClosureIfNeeded($overrides, $routeTargetConfig);
if (!is_array($overrides)) {
throw new \Exception(sprintf("Override closure for %s:%s -> %s:%s must return an array", $sourceController, $sourceAction, $targetController, $targetAction), 1);
}
@ -332,7 +382,7 @@ class BreadcrumbFactory
{
$routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true);
$routeTargetConfig = $this->getRouteConfig($targetController, $targetAction);
$overrides = $this->execClosureIfNeeded($overrides, $routeSourceConfig);
$overrides = $this->execClosureIfNeeded($overrides, $routeTargetConfig);
if (is_null($overrides)) {
// Overrides is null, the link should not be added
return;
@ -348,20 +398,27 @@ class BreadcrumbFactory
public function addCustomLink(string $sourceController, string $sourceAction, string $targetUrl, string $label, $overrides = [])
{
$routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true);
$links = array_merge($routeSourceConfig['links'] ?? [], [[
$overrides = $this->execClosureIfNeeded($overrides, $routeSourceConfig);
if (!is_array($overrides)) {
throw new \Exception(sprintf("Override closure for custom action %s:%s must return an array", $sourceController, $sourceAction), 1);
}
$linkConfig = [
'url' => $targetUrl,
'icon' => 'link',
'label' => $label,
'route_path' => 'foo:bar'
]]);
];
$linkConfig = array_merge($linkConfig, $overrides);
$links = array_merge($routeSourceConfig['links'] ?? [], [$linkConfig]);
$this->endpoints[$sourceController][$sourceAction]['links'] = $links;
}
public function addAction(string $sourceController, string $sourceAction, string $targetController, string $targetAction, $overrides = [])
{
$routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true);
$routeTargetConfig = $this->getRouteConfig($targetController, $targetAction);
$overrides = $this->execClosureIfNeeded($overrides, $routeSourceConfig);
$overrides = $this->execClosureIfNeeded($overrides, $routeTargetConfig);
if (!is_array($overrides)) {
throw new \Exception(sprintf("Override closure for %s:%s -> %s:%s must return an array", $sourceController, $sourceAction, $targetController, $targetAction), 1);
}
@ -370,6 +427,33 @@ class BreadcrumbFactory
$this->endpoints[$sourceController][$sourceAction]['actions'] = $links;
}
/**
* Add a custom action to the action bar
*
* @param string $sourceController The source controller name
* @param string $sourceAction The source action name
* @param string $targetUrl The target URL for that action
* @param string $label The text to be displayed in the button
* @param array $overrides Optional overrides to apply on this action
* @return void
*/
public function addCustomAction(string $sourceController, string $sourceAction, string $targetUrl, string $label, $overrides = [])
{
$routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true);
$overrides = $this->execClosureIfNeeded($overrides, $routeSourceConfig);
if (!is_array($overrides)) {
throw new \Exception(sprintf("Override closure for custom action %s:%s must return an array", $sourceController, $sourceAction), 1);
}
$actionConfig = [
'url' => $targetUrl,
'label' => $label,
'route_path' => 'foo:bar'
];
$actionConfig = array_merge($actionConfig, $overrides);
$links = array_merge($routeSourceConfig['actions'] ?? [], [$actionConfig]);
$this->endpoints[$sourceController][$sourceAction]['actions'] = $links;
}
public function removeLink(string $sourceController, string $sourceAction, string $targetController, string $targetAction)
{
$routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true);
@ -384,6 +468,35 @@ class BreadcrumbFactory
}
}
public function removeAction(string $sourceController, string $sourceAction, string $targetController, string $targetAction)
{
$routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true);
if (!empty($routeSourceConfig['actions'])) {
foreach ($routeSourceConfig['actions'] as $i => $routeConfig) {
if ($routeConfig['controller'] == $targetController && $routeConfig['action'] == $targetAction) {
unset($routeSourceConfig['actions'][$i]);
$this->endpoints[$sourceController][$sourceAction]['actions'] = $routeSourceConfig['actions'];
break;
}
}
}
}
public function registerGoToMenuConfig(string $sourceController, string $sourceAction, string $goToID, array $config = []): void
{
$this->endpoints[$sourceController][$sourceAction]['goToMenu'][$goToID] = $config;
}
public function registerLinkMenuConfig(string $sourceController, string $sourceAction, string $menuID, array $config = []): void
{
$this->endpoints[$sourceController][$sourceAction]['linkMenu'][$menuID] = $config;
}
public function registerActionMenuConfig(string $sourceController, string $sourceAction, string $menuID, array $config = []): void
{
$this->endpoints[$sourceController][$sourceAction]['actionMenu'][$menuID] = $config;
}
public function getRouteConfig($controller, $action, $fullRoute = false)
{
$routeConfig = $this->get($controller, $action);

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,7 @@ use Cake\Http\Exception\NotFoundException;
class UsersController extends AppController
{
public $filterFields = ['email', 'Organisations.name', 'Organisations.id'];
public $filterFields = ['email', 'autoalert', 'contactalert', 'termsaccepted', 'disabled', 'role_id' ,'Organisations.name', 'Roles.name', ];
public $quickFilterFields = [['email' => true]];
public $containFields = ['Roles', /*'UserSettings',*/ 'Organisations'];
@ -26,10 +26,36 @@ class UsersController extends AppController
if (!empty(Configure::read('keycloak.enabled'))) {
// $keycloakUsersParsed = $this->Users->getParsedKeycloakUser();
}
$roles = $this->Users->Roles->find('list')->all()->toArray();
$roleFilters = [];
foreach ($roles as $roleID => $roleName) {
$roleFilters[] = [
'label' => __('{0}', h($roleName)),
'filterCondition' => ['role_id' => h($roleID)],
];
}
$rolesForContext = [[
'is_group' => true,
'icon' => $this->Navigation->iconToTableMapping['Roles'],
'label' => __('Roles'),
'filters' => $roleFilters,
]];
$this->CRUD->index([
'contain' => $this->containFields,
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contextFilters' => [
'custom' => array_merge([
[
'label' => __('Active'),
'filterCondition' => ['disabled' => 0],
],
[
'label' => __('Disabled'),
'filterCondition' => ['disabled' => 1],
]
], $rolesForContext),
],
'conditions' => $conditions,
'afterFind' => function($data) use ($keycloakUsersParsed) {
// TODO: We might want to uncomment this at some point Still need to evaluate the impact
@ -52,6 +78,19 @@ class UsersController extends AppController
$this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate');
}
public function filtering()
{
$this->CRUD->filtering([
'afterFind' => function ($filtersConfig, $typeMap) {
$newFilterConfig = $this->CRUD->transformBooleanFieldsIntoRadio($filtersConfig, $typeMap);
return [
'typeMap' => $typeMap,
'filtersConfig' => $newFilterConfig,
];
}
]);
}
public function add()
{
$currentUser = $this->ACL->getUser();

View File

@ -67,7 +67,7 @@ class UsersTable extends AppTable
'propertyName' => 'UserSetting'
]
);
$this->setDisplayField('username');
$this->setDisplayField('email');
}
public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)

32
src/Utility/Utils.php Normal file
View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,92 @@
<?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
* - attrs: Additional HTML attributes
*
* # 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' => [],
'attrs' => [],
];
private $bsHelper;
function __construct(array $options, $bsHelper)
{
$this->allowedOptionValues = [
'variant' => BootstrapGeneric::$variants,
];
$this->processOptions($options);
$this->bsHelper = $bsHelper;
}
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', array_merge([
'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->options['attrs']), [
$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' : '')]
], $this->bsHelper);
return $bsIcon->icon();
}
return '';
}
}

View File

@ -0,0 +1,145 @@
<?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 $bsHelper;
private $bsClasses = [];
function __construct(array $options, $bsHelper)
{
$this->allowedOptionValues = [
'variant' => array_merge(BootstrapGeneric::$variants, ['link', 'text']),
'size' => ['', 'xs', 'sm', 'lg'],
'type' => ['button', 'submit', 'reset']
];
$this->processOptions($options);
$this->bsHelper = $bsHelper;
}
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'], $this->bsHelper);
$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' : '')]
], $this->bsHelper);
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 '';
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,247 @@
<?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',
* 'variant' => 'primary',
* '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' => [],
];
private $menu;
private $btHelper;
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'];
if (!empty($this->options['button']['split'])) {
$classes[] = 'btn-group';
}
$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);
if (!empty($this->options['button']['split'])) {
$optionsMainButton = $options;
unset($optionsMainButton['attrs']['data-bs-toggle']);
unset($optionsMainButton['attrs']['aria-expanded']);
$optionsMainButton['class'] = array_filter($optionsMainButton['class'], function($classname) {
return $classname != 'dropdown-toggle';
});
$optionsSplitButton = [
'variant' => $options['variant'],
'outline' => $options['outline'],
'badge' => $options['badge'],
'size' => $options['size'],
'class' => ['dropdown-toggle dropdown-toggle-split'],
'attrs' => $options['attrs'],
];
$html = $this->btHelper->button($optionsMainButton);
$html .= $this->btHelper->button($optionsSplitButton);
return $html;
} else {
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-1 dropdown-item-icon']);
}
$badge = '';
if (!empty($entry['badge'])) {
$bsBadge = new BootstrapBadge(array_merge(
['class' => ['ms-auto']],
$entry['badge']
), $this->btHelper);
$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']);
}
if (!empty($entry['variant'])) {
if (empty($entry['outline'])) {
$classes[] = "dropdown-item-{$entry['variant']}";
} else {
$classes[] = "dropdown-item-outline-{$entry['variant']}";
}
}
$params = $entry['attrs'] ?? [];
$params['href'] = !empty($params['href']) ? $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);
}
}

View File

@ -0,0 +1,68 @@
<?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
{
public $helpers = ['Icon'];
private $icon = '';
private $bsHelper;
private $defaultOptions = [
'id' => '',
'class' => [],
'title' => '',
'attrs' => [],
];
function __construct($icon, array $options = [], $bsHelper)
{
$this->icon = $icon;
$this->processOptions($options);
$this->bsHelper = $bsHelper;
}
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
{
$options = [
'id' => $this->options['id'] ?? '',
'class' => implode('', $this->options['class']),
'title' => h($this->options['title']),
];
$options = array_merge($this->options['attrs'], $options);
if (is_array($this->icon)) {
$options = array_merge($options, $this->icon);
} else {
$options = array_merge($options, ['icon' => $this->icon]);
}
$html = $this->bsHelper->Icon->icon($options);
return $html;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,244 @@
<?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
* - keyClass: A list of class to be added to all keys
* - valueClass: A list of class to be added to all valkue cell
* - 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
* - rowClass: A list of class to be added to the row
* - keyClass: A list of class to be added to the key cell
* - valueClass: A list of class to be added to the value cell
*
* # 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,
'fluid' => false,
'variant' => '',
'tableClass' => [],
'bodyClass' => [],
'rowClass' => [],
'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->options['rowClass'] = $this->convertToArrayIfNeeded($this->options['rowClass']);
$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
{
$allKeyClass = $this->convertToArrayIfNeeded($this->options['keyClass'] ?? []);
$keyClass = $this->convertToArrayIfNeeded($field['keyClass'] ?? []);
$rowValue = $this->genCell($field);
$rowKey = $this->node('th', [
'class' => array_merge(
$allKeyClass,
$keyClass,
!empty($this->options['fluid']) ? ['col flex-shrink-1'] : ['col-4 col-sm-3'],
),
'scope' => 'row'
], $field['keyHtml'] ?? h($field['key']));
$row = $this->node('tr', [
'class' => array_merge(
$this->options['rowClass'],
[
'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"]);
}
}
$allValueClass = $this->convertToArrayIfNeeded($this->options['valueClass'] ?? []);
$valueClass = $this->convertToArrayIfNeeded($field['valueClass'] ?? []);
return $this->node('td', [
'class' => array_merge(
$allValueClass,
$valueClass,
[
(!empty($this->options['fluid']) ? 'col flex-grow-1' : 'col-8 col-sm-9'),
!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)) : '';
}
}

View File

@ -0,0 +1,351 @@
<?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' => ''
];
private $bsHelper;
function __construct(array $options, $bsHelper)
{
$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);
$this->bsHelper = $bsHelper;
}
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',
],
], $this->bsHelper))->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, ), $this->bsHelper)->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, $this->bsHelper))->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, $this->bsHelper))->button();
}
return implode('', $buttons);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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)) : '';
}
}

View File

@ -0,0 +1,303 @@
<?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']}",
]
)], $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']}",
]
)], $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' : '')
],
[
]
)], $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);
}
}

View File

@ -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

View File

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

View File

@ -0,0 +1,72 @@
<?php
namespace App\View\Helper;
use Cake\View\Helper;
use Cake\Utility\Hash;
class IconHelper extends Helper
{
public $helpers = ['FontAwesome', 'Bootstrap'];
public function icon($icon)
{
if (!empty($icon['icons']) || !empty($icon['stacked'])) {
if (!empty($icon['stacked'])) {
$icon['icons'] = $icon['stacked'];
}
return $this->stackedIcons($icon);
} else if (!empty($icon['image'])) {
return $this->image($icon);
} else if (!empty($icon['html'])) {
return $this->rawHtml($icon['html']);
}
return $this->regularIcon($icon);
}
public function regularIcon($icon)
{
return $this->Bootstrap->node('i', [
'class' => h($icon['class'] ?? '') . ' ' .$this->FontAwesome->getClass($icon['icon'] ?? $icon),
'style' => h($icon['style'] ?? ''),
'title' => h($icon['title'] ?? null)
]);
}
public function stackedIcons($icons)
{
$options = $icons;
$icons = $icons['icons'];
$html = $this->Bootstrap->node('span', [
'class' => sprintf('fa-stack fa-stack-small %s', h($options['class'] ?? '')),
'style' => h($options['style'] ?? '')
],
implode('', [
$this->Bootstrap->node('span', [
'class' => sprintf('fas fa-stack-2x fa-%s %s', h($icons[0]['icon'] ?? ''), h($icons[0]['class'] ?? '')),
'style' => h($icons[0]['style'] ?? ''),
]),
$this->Bootstrap->node('span', [
'class' => sprintf('fas fa-stack-1x fa-%s %s', h($icons[1]['icon'] ?? ''), h($icons[1]['class'] ?? '')),
'style' => h($icons[1]['style'] ?? ''),
])
])
);
return $html;
}
public function image($image)
{
return $this->Bootstrap->node('img', [
'class' => h($image['class'] ?? ''),
'style' => h($image['style'] ?? ''),
'src' => h($image['image'] ?? ''),
'title' => h($image['title'] ?? null)
]);
}
public function rawHtml($html)
{
return $html;
}
}

View File

@ -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,

View File

@ -114,7 +114,10 @@ echo $this->element('genericElements/IndexTable/index_table', [
'type' => 'simple',
'text' => __('Add role'),
'class' => 'btn btn-primary',
'popover_url' => '/roles/add'
'popover_url' => '/roles/add',
'button' => [
'icon' => 'plus',
]
]
]
],

View File

@ -15,7 +15,10 @@
'type' => 'simple',
'text' => __('Add entry'),
'class' => 'btn btn-primary',
'popover_url' => '/admin/allowedlists/add'
'popover_url' => '/admin/allowedlists/add',
'button' => [
'icon' => 'plus',
]
]
]
],

26
templates/Events/view.php Normal file
View File

@ -0,0 +1,26 @@
<h2 class="fw-light text-truncate"><?= h($entity['Event']['info']) ?></h2>
<div class="mb-2">
<?= $this->element('Events/event-critical-notices') ?>
</div>
<div class="d-flex flex-row gap-2 mb-3">
<div style="flex-basis: 33%;">
<?= $this->element('Events/event-metadata') ?>
</div>
<div class="flex-grow-1">
<?= $this->element('Events/event-context') ?>
</div>
</div>
<div class="mb-3">
<?= $this->element('Events/event-stats') ?>
</div>
<div class="mb-2">
<?= $this->element('Events/event-notices') ?>
</div>
<div class="mb-2">
<?= $this->element('Events/event-content') ?>
</div>

View File

@ -10,14 +10,15 @@ echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'type' => 'simple',
'text' => __('Add organisation'),
'class' => 'btn btn-primary',
'popover_url' => '/organisations/add'
'popover_url' => '/organisations/add',
'button' => [
'icon' => 'plus',
]
]
]
],
[
'type' => 'context_filters',
'context_filters' => $filteringContexts
],
[
'type' => 'search',
@ -87,13 +88,14 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
*/
],
'primary_id_path' => 'id',
'title' => __('Organisation Index'),
'description' => __('A list of organisations known to your MISP instance. This list can get populated either directly, by adding new organisations or by fetching them from trusted remote sources.'),
'actions' => [
[
'url' => '/organisations/view',
'url_params_data_paths' => ['id'],
'icon' => 'eye'
'icon' => 'eye',
],
[
'open_modal' => '/organisations/edit/[onclick_params_data_path]',

View File

@ -115,7 +115,10 @@ echo $this->element('genericElements/IndexTable/index_table', [
'type' => 'simple',
'text' => __('Add role'),
'class' => 'btn btn-primary',
'popover_url' => '/roles/add'
'popover_url' => '/roles/add',
'button' => [
'icon' => 'plus',
]
]
]
],

View File

@ -8,77 +8,63 @@ echo $this->element('genericElements/IndexTable/index_table', [
'top_bar' => [
'children' => [
[
'type' => 'multi_select_actions',
'force-dropdown' => true,
'children' => [
['is-header' => true, 'text' => __('Toggle selected users'), 'icon' => 'user-times',],
[
'class' => 'd-none mass-select',
'text' => __('Disable selected users'),
'onClick' => "multiSelectToggleField",
'onClickParams' => ['admin/users', 'massToggleField', 'disabled', '1', '#UserUserIds']
'text' => __('Disable users'),
'variant' => 'warning',
'outline' => true,
'onclick' => 'disableUsers',
],
[
'class' => 'd-none mass-select',
'text' => __('Enable selected users'),
'onClick' => "multiSelectToggleField",
'onClickParams' => ['admin/users', 'massToggleField', 'disabled', '0', '#UserUserIds']
'text' => __('Enable users'),
'variant' => 'success',
'outline' => true,
'onclick' => 'enableUsers',
],
['is-header' => true, 'text' => __('Publishing alert'), 'icon' => 'bell',],
[
'text' => __('Disable publishing emailing'),
'onclick' => 'disablePublishingEmailing',
],
[
'class' => 'd-none mass-select',
'text' => __('Disable publish emailing'),
'onClick' => "multiSelectToggleField",
'onClickParams' => ['admin/users', 'massToggleField', 'autoalert', '0', '#UserUserIds']
],
[
'class' => 'd-none mass-select',
'text' => __('Enable publish emailing'),
'onClick' => "multiSelectToggleField",
'onClickParams' => ['admin/users', 'massToggleField', 'autoalert', '1', '#UserUserIds']
'text' => __('Enable publishing emailing'),
'onclick' => 'enablePublishingEmailing',
],
],
'data' => [
'id' => [
'value_path' => 'id'
]
]
],
[
'type' => 'simple',
'children' => [
'data' => [
'id' => 'create-button',
'title' => __('Modify filters'),
'fa-icon' => 'search',
'onClick' => 'getPopup',
'onClickParams' => [$urlparams, 'users', 'filterUserIndex']
/*
'type' => 'simple',
'icon' => 'plus',
'text' => __('Add User'),
'class' => 'btn btn-primary',
'popover_url' => '/users/add'
*/
'popover_url' => '/users/add',
'button' => [
'icon' => 'plus',
]
]
]
],
[
'type' => 'simple',
'children' => [
[
'url' => $baseurl . '/admin/users/index',
'text' => __('All'),
'active' => !isset($passedArgsArray['disabled']),
],
[
'url' => $baseurl . '/admin/users/index/searchdisabled:0',
'text' => __('Active'),
'active' => isset($passedArgsArray['disabled']) && $passedArgsArray['disabled'] === "0",
],
[
'url' => $baseurl . '/admin/users/index/searchdisabled:1',
'text' => __('Disabled'),
'active' => isset($passedArgsArray['disabled']) && $passedArgsArray['disabled'] === "1",
]
]
'type' => 'context_filters',
],
[
'type' => 'search',
'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value'
'searchKey' => 'value',
'allowFilering' => true
],
[
'type' => 'table_action',
@ -86,15 +72,6 @@ echo $this->element('genericElements/IndexTable/index_table', [
]
],
'fields' => [
[
'element' => 'selector',
'class' => 'short',
'data' => [
'id' => [
'value_path' => 'id'
]
]
],
[
'name' => __('ID'),
'sort' => 'id',
@ -268,7 +245,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
'url_params_vars' => ['id'],
'toggle_data' => [
'editRequirement' => [
'function' => function($row, $options) {
'function' => function ($row, $options) {
return true;
},
],
@ -334,5 +311,41 @@ echo $this->element('genericElements/IndexTable/index_table', [
]
]
]);
echo '</div>';
?>
<script>
function enableUsers(idList, selectedData, $table) {
return massToggle('disabled', false, idList, selectedData, $table)
}
function disableUsers(idList, selectedData, $table) {
return massToggle('disabled', true, idList, selectedData, $table)
}
function enablePublishingEmailing(idList, selectedData, $table) {
return massToggle('autoalert', true, idList, selectedData, $table)
}
function disablePublishingEmailing(idList, selectedData, $table) {
return massToggle('autoalert', false, idList, selectedData, $table)
}
function reloadOnSuccess([data, modalObject]) {
UI.reload('<?= $baseurl ?>/users/index', UI.getContainerForTable($table), $table)
}
function callbackOnFailure([data, modalObject]) {
console.error(data)
}
function massToggle(field, enabled, idList, selectedData, $table) {
const successCallback = reloadOnSuccess
const failCallback = callbackOnFailure
const url = `<?= $baseurl ?>/users/massToggleField/${field}:${enabled ? 1 : 0}/`
UI.submissionModal(url, successCallback, failCallback)
.then(([modalObject, ajaxApi]) => {
const $idsInput = modalObject.$modal.find('form').find('input#ids-field')
$idsInput.val(JSON.stringify(idList))
})
}
</script>

View File

@ -9,7 +9,7 @@ use Cake\Core\Configure;
$this->Html->image('misp-logo.png', [
'alt' => __('MISP logo'),
'height' => 100,
'style' => ['filter: drop-shadow(4px 4px 4px #924da666);']
'style' => ['filter: drop-shadow(4px 4px 4px #22222233);']
])
);
$template = [
@ -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('email', ['label' => 'Email', 'class' => 'form-control mb-2', 'placeholder' => __('Email')]);
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();
}

View File

@ -0,0 +1,73 @@
<?php
$objectHtml = '<i>objects</i>';
$attributeHtml = '<b>attributes</b>';
$reporttHtml = 'reports';
$eventGraphHtml = 'eventgraph';
$timelineHtml = 'timeline';
$attackHtml = 'Include matrix, preventive measures & mitigations ';
$discussionHtml = 'discussion';
echo $this->Bootstrap->tabs([
'horizontal-position' => 'top',
'fill-header' => true,
'card' => true,
'data' => [
'navs' => [
[
'html' => sprintf(
'%s %s',
$this->Bootstrap->icon($iconToTableMapping['Objects']),
__('Objects') . $this->Bootstrap->badge(['text' => $stats['stat_counts']['objects'],
'variant' => 'primary',
])
),
'active' => true,
],
[
'html' => sprintf(
'%s %s',
$this->Bootstrap->icon($iconToTableMapping['Attributes']),
__('Attributes') . $this->Bootstrap->badge(['text' => $stats['stat_counts']['attributes'],
'variant' => 'primary',
])
),
],
[
'html' => sprintf(
'%s %s',
$this->Bootstrap->icon($iconToTableMapping['EventReports']),
__('Reports') . $this->Bootstrap->badge(['text' => $stats['stat_counts']['eventreports'],
'variant' => $stats['stat_counts']['eventreports'] > 0 ? 'warning' : 'primary',
])
),
],
['html' => sprintf('%s %s', $this->Bootstrap->icon('diagram-project'), __('Event Graph')),],
['html' => sprintf('%s %s', $this->Bootstrap->icon('timeline'), __('Event Timeline')),],
[
'html' => $this->Bootstrap->node('span', [
'class' => ['text-uppercase'],
'style' => 'color: #C8452B;'
], 'ATT&CK' . '<sup>®</sup>'),
],
[
'html' => sprintf(
'%s %s',
$this->Bootstrap->icon('comments'),
__('Discussion') . $this->Bootstrap->badge(['text' => $stats['stat_counts']['discussions'],
'variant' => $stats['stat_counts']['discussions'] > 0 ? 'warning' : 'primary',
])
),
],
],
'content' => [
$objectHtml,
$attributeHtml,
$reporttHtml,
$eventGraphHtml,
$timelineHtml,
$attackHtml,
$discussionHtml,
]
]
]);

View File

@ -0,0 +1,304 @@
<?php
use Cake\Core\Configure;
$content = '';
$event = $entity;
$warningslist_hits = $warningslist_hits;
$contributors = [];
$instanceFingerprint = '???';
$hostOrgUser = true;
$extended = false;
$fields = [
[
'key' => __('Threat Level'),
// 'path' => 'ThreatLevel.name',
// 'key_title' => $eventDescriptions['threat_level_id']['desc'],
// 'class' => 'threat-level-' . h(strtolower($event['ThreatLevel']['name']))
'path' => 'Event.threat_level_id',
],
[
'key' => __('Analysis'),
// 'key_title' => $eventDescriptions['analysis']['desc'],
'path' => 'Event.analysis',
// 'type' => 'mapping',
// 'mapping' => $analysisLevels
],
[
'key' => __('Tags'),
'type' => 'custom',
'function' => function (array $event, $viewContext) {
$html = '';
foreach ($event['Event']['Tag'] as $tag) {
if (str_starts_with($tag['name'], 'misp-galaxy:')) {
continue;
}
$html .= $viewContext->Bootstrap->badge([
'text' => $tag['name'],
'class' => ['rounded-1'],
'attrs' => [
'style' => [
"background-color: {$tag['colour']} !important;",
"color: white !important;",
"margin: 1px !important;"
],
],
]);
}
return $html;
}
// 'function' => function (array $event) use ($isSiteAdmin, $mayModify, $me, $missingTaxonomies, $tagConflicts) {
// return sprintf(
// '<span class="eventTagContainer">%s</span>',
// $this->element(
// 'ajaxTags',
// [
// 'event' => $event,
// 'tags' => $event['EventTag'],
// 'tagAccess' => $isSiteAdmin || $mayModify,
// 'localTagAccess' => $this->Acl->canModifyTag($event, true),
// 'missingTaxonomies' => $missingTaxonomies,
// 'tagConflicts' => $tagConflicts
// ]
// )
// );
// }
],
[
'key' => __('Galaxies'),
'type' => 'custom',
'function' => function (array $event, $viewContext) {
$html = '';
foreach ($event['Event']['Tag'] as $tag) {
if (!str_starts_with($tag['name'], 'misp-galaxy:')) {
continue;
}
$html .= $viewContext->Bootstrap->badge([
'text' => $tag['name'],
'class' => ['rounded-1'],
'attrs' => [
'style' => [
"background-color: {$tag['colour']} !important;",
"color: white !important;",
"margin: 1px !important;"
],
],
]);
}
return $html;
}
],
[
'key' => __('Extends'),
// 'type' => 'extends',
'path' => 'Event.extends_uuid',
'extendedEvent' => isset($extendedEvent) ? $extendedEvent : null,
'class' => 'break-word',
'requirement' => !empty($extendedEvent)
],
[
'key' => __('Extended by'),
// 'type' => 'extendedBy',
'path' => 'Event.id',
'extended_by' => isset($extensions) ? $extensions : null,
'extended' => $extended,
'class' => 'break-word',
'requirement' => !empty($extensions)
],
[
'key' => __('Related Events'),
'type' => 'custom',
'function' => function ($event, $viewContext) {
$text = __('{0} related hits', count($event['Event']['RelatedEvent'] ?? []));
if (count($event['Event']['RelatedEvent'] ?? []) == 0) {
return $this->Bootstrap->node('span', [
'class' => ['fs-7 text-muted'],
], $text);
}
$table = $this->Bootstrap->table(
[
'hover' => false,
'striped' => false,
'class' => ['event-context-related', 'mb-0'],
],
[
'items' => $event['Event']['RelatedEvent'],
'fields' => [
[
'path' => 'Event.info',
'label' => __('Name'),
'class' => ['fw-light']
],
[
'path' => 'Event.date',
'label' => __('Date'),
],
[
'path' => 'Event.Org.name',
'label' => __('Org'),
],
],
]
);
return $viewContext->Bootstrap->collapse([
'button' => [
'text' => $text,
'variant' => 'link',
'size' => 'sm',
'class' => 'p-0'
],
], $table);
}
],
[
'key' => __('Feed Hits'),
'type' => 'custom',
'function' => function ($event, $viewContext) {
$text = __('{0} feed hits', count($event['Event']['Feed'] ?? []));
if (count($event['Event']['Feed'] ?? []) == 0) {
return $this->Bootstrap->node('span', [
'class' => ['fs-7 text-muted'],
], $text);
}
$table = $this->Bootstrap->table(
[
'hover' => false,
'striped' => false,
'class' => ['event-context-feed', 'mb-0'],
],
[
'items' => $event['Event']['Feed'],
'fields' => [
[
'path' => 'name',
'label' => __('Name'),
'class' => ['fw-light']
],
[
'path' => 'provider',
'label' => __('Provider'),
],
],
]
);
return $viewContext->Bootstrap->collapse([
'button' => [
'text' => $text,
'variant' => 'link',
'size' => 'sm',
'class' => 'p-0'
],
], $table);
}
],
[
'key' => __('Server Hits'),
'type' => 'custom',
'function' => function ($event, $viewContext) {
$text = __('{0} server hits', count($event['Event']['Server'] ?? []));
if (count($event['Event']['Server'] ?? []) == 0) {
return $this->Bootstrap->node('span', [
'class' => ['fs-7 text-muted'],
], $text);
}
$table = $this->Bootstrap->table(
[
'hover' => false,
'striped' => false,
'class' => ['event-context-feed', 'mb-0'],
],
[
'items' => $event['Event']['Server'] ?? [],
'fields' => [
[
'path' => 'name',
'label' => __('Name'),
'class' => ['fw-light']
],
[
'path' => 'provider',
'label' => __('Provider'),
],
],
]
);
return $viewContext->Bootstrap->collapse([
'button' => [
'text' => $text,
'variant' => 'link',
'size' => 'sm',
'class' => 'p-0'
],
], $table);
}
],
[
'key' => __('Warninglist Hits'),
'type' => 'custom',
'rowVariant' => !empty($warningslist_hits) ? 'danger' : '',
'function' => function ($event, $viewContext) use ($warningslist_hits) {
$text = __('{0} warninglist hits', count($warningslist_hits ?? []));
if (count($warningslist_hits ?? []) == 0) {
return $this->Bootstrap->node('span', [
'class' => ['fs-7 text-muted'],
], $text);
}
$table = $this->Bootstrap->table(
[
'hover' => false,
'striped' => false,
'class' => ['event-context-warninglist', 'mb-0'],
],
[
'items' => $warningslist_hits,
'fields' => [
[
'path' => 'warninglist_name',
'label' => __('Name'),
'class' => ['fw-light']
],
[
'path' => 'warninglist_category',
'label' => __('Category'),
],
],
]
);
return $viewContext->Bootstrap->collapse([
'button' => [
'text' => __('{0} warninglist hits', count($warningslist_hits)),
'variant' => 'link',
'size' => 'sm',
'class' => 'p-0'
],
], $table);
}
],
];
$tableRandomValue = Cake\Utility\Security::randomString(8);
$listTableOptions = [
'id' => "single-view-table-{$tableRandomValue}",
'hover' => false,
'fuild' => true,
'tableClass' => ['event-context', 'mb-0'],
'elementsRootPath' => '/genericElements/SingleViews/Fields/'
];
$listTable = $this->Bootstrap->listTable($listTableOptions, [
'item' => $entity,
'fields' => $fields
]);
$content = $listTable;
echo $this->Bootstrap->card([
'bodyHTML' => $content,
'bodyClass' => 'p-0',
'class' => ['shadow-md'],
]);

View File

@ -0,0 +1,7 @@
<?php
echo $this->Bootstrap->alert([
'text' => 'critical notice like delegation requests, tag conflicts, ... ',
'class' => [''],
'dismissible' => true,
'variant' => 'danger',
]);

View File

@ -0,0 +1,164 @@
<?php
use Cake\Core\Configure;
$content = '';
$event = $entity;
$contributors = [];
$instanceFingerprint = '???';
$hostOrgUser = true;
$fields = [
[
'key' => __('Event ID'),
'path' => 'Event.id'
],
[
'key' => 'UUID',
'path' => 'Event.uuid',
'valueClass' => 'quickSelect fw-light fs-7',
// 'type' => 'uuid',
'action_buttons' => [
[
'url' => $baseurl . '/events/add/extends:' . h($event['Event']['uuid']),
'icon' => 'plus-square',
'style' => 'color:black; font-size:15px;padding-left:2px',
'title' => __('Extend this event'),
// 'requirement' => $this->Acl->canAccess('events', 'add'),
'requirement' => true,
],
[
'url' => $baseurl . '/servers/idTranslator/' . h($event['Event']['id']),
'icon' => 'server',
'style' => 'color:black; font-size:15px;padding-left:2px',
'title' => __('Check this event on different servers'),
// 'requirement' => $this->Acl->canAccess('servers', 'idTranslator'),
'requirement' => true,
]
]
],
[
'key' => __('Creator org'),
// 'type' => 'org',
// 'path' => 'Orgc',
'path' => 'Event.Orgc.name',
'element' => 'org',
'requirement' => empty(Configure::read('MISP.showorgalternate'))
],
[
'key' => __('Owner org'),
// 'type' => 'org',
// 'path' => 'Org',
'path' => 'Event.Org.name',
'element' => 'org',
'requirement' => $isSiteAdmin && empty(Configure::read('MISP.showorgalternate'))
],
[
'key' => __('Contributors'),
'type' => 'custom',
'function' => function (array $event) use ($contributors, $baseurl) {
$contributorsContent = [];
foreach ($contributors as $organisationId => $name) {
$org = ['Organisation' => ['id' => $organisationId, 'name' => $name]];
if (Configure::read('MISP.log_new_audit')) {
$link = $baseurl . "/audit_logs/eventIndex/" . h($event['Event']['id']) . '/' . h($organisationId);
} else {
$link = $baseurl . "/logs/event_index/" . h($event['Event']['id']) . '/' . h($name);
}
$contributorsContent[] = $this->OrgImg->getNameWithImg($org, $link);
}
return implode('<br>', $contributorsContent);
},
'requirement' => !empty($contributors)
],
[
'key' => __('Creator user'),
// 'path' => 'User.email',
'path' => 'Event.event_creator_email',
'requirement' => isset($event['User']['email'])
],
[
'key' => __('Protected Event'),
'key_info' => __(
"Protected events carry a list of cryptographic keys used to sign and validate the information in transit.\n\nWhat this means in practice, a protected event shared with another instance will only be able to receive updates via the synchronisation mechanism from instances that are able to provide a valid signature from the event's list of signatures.\n\nFor highly critical events in broader MISP networks, this can provide an additional layer of tamper proofing to ensure that the original source of the information maintains control over modifications. Whilst this feature has its uses, it is not required in most scenarios."
),
'path' => 'CryptographicKey',
'event_path' => 'Event',
'owner' => ((int)$loggedUser['org_id'] === (int)$event['Event']['orgc_id'] &&
$hostOrgUser &&
!$event['Event']['locked']
),
'instanceFingerprint' => $instanceFingerprint,
// 'type' => 'protectedEvent'
],
[
'key' => __('Date'),
'path' => 'Event.date'
],
[
'key' => __('Distribution'),
'path' => 'Event.distribution',
'sg_path' => 'SharingGroup',
'event_id_path' => 'Event.id',
// 'type' => 'distribution'
],
[
'key' => __('Published'),
// 'path' => 'Event.published',
'key_class' => ($event['Event']['published'] == 0) ? 'not-published' : 'published',
'class' => ($event['Event']['published'] == 0) ? 'not-published' : 'published',
'rowVariant' => $event['Event']['published'] == 0 ? 'warning' : '',
'type' => 'custom',
'function' => function (array $event) {
if (!$event['Event']['published']) {
$string = '<span class="label label-important label-padding">' . __('No') . '</span>';
if (!empty($event['Event']['publish_timestamp'])) {
$string .= __(' (last published at %s)', $this->Time->time($event['Event']['publish_timestamp']));
}
return $string;
} else {
return sprintf(
'<span class="label label-success label-padding">%s</span> %s',
__('Yes'),
empty($event['Event']['publish_timestamp']) ? __('N/A') : $this->Time->time($event['Event']['publish_timestamp'])
);
}
}
],
];
$tableRandomValue = Cake\Utility\Security::randomString(8);
$listTableOptions = [
'id' => "single-view-table-{$tableRandomValue}",
'hover' => false,
'fluid' => true,
'tableClass' => ['event-metadata', 'mb-0'],
'keyClass' => ['event-metadata-key-cell'],
'elementsRootPath' => '/genericElements/SingleViews/Fields/'
];
$listTable = $this->Bootstrap->listTable($listTableOptions, [
'item' => $entity,
'fields' => $fields
]);
// $eventInfo = $this->Bootstrap->node('div', [
// 'id' => 'event-info',
// 'class' => ['py-2 px-1', 'fw-light fs-7'],
// ], h($event['Event']['info']));
$eventInfo = '';
$content = $eventInfo . $listTable;
echo $this->Bootstrap->card([
'bodyHTML' => $content,
'bodyClass' => 'p-0',
'class' => ['shadow-md'],
]);
?>
<style>
.event-metadata .event-metadata-key-cell {
min-width: 6em;
}
</style>

View File

@ -0,0 +1,7 @@
<?php
echo $this->Bootstrap->alert([
'text' => 'notice for empty event, ...',
'class' => [''],
'dismissible' => true,
'variant' => 'warning',
]);

View File

@ -0,0 +1,376 @@
<?php
$eventid = 2;
$warningslist_hits = $warningslist_hits;
$recent_sightings = [
[(time() - 8 * 86400) * 1000, 1], [(time() - 7 * 86400) * 1000, 2], [(time() - 3 * 86400) * 1000, 1],
];
$stat_distribution = array_map(function ($e) use ($stats) {
return ceil(100 * $e / $stats['stat_counts']['attributes']);
}, array_filter($stats['stat_distribution']));
?>
<div class="stat-container container-fluid ps-0 pe-0">
<div class="row g-2">
<div class="stat-panel-md">
<?php
$chartActivityHtml = $this->element('charts/generic', [
'series' => [
['data' => [12, 14, 2, 15, 47, 75, 65, 19, 14]]
],
'chartOptions' => [
'chart' => [
'type' => 'bar',
'height' => '160',
'toolbar' => [
'show' => false,
],
'zoom' => [
'enabled' => false,
],
],
'stroke' => [
'curve' => 'straight'
],
'title' => [
'text' => __('Event activity'),
],
'labels' => [
(time() - 8 * 86400) * 1000, (time() - 7 * 86400) * 1000, (time() - 6 * 86400) * 1000, (time() - 5 * 86400) * 1000, (time() - 4 * 86400) * 1000, (time() - 3 * 86400) * 1000, (time() - 2 * 86400) * 1000, (time() - 1 * 86400) * 1000, time() * 1000
],
'grid' => [
'show' => true,
'yaxis' => [
'lines' => [
'show' => true,
],
],
],
'yaxis' => [
'show' => false,
],
'xaxis' => [
'type' => 'datetime',
],
'dataLabels' => [
'enabled' => false,
],
'tooltip' => [
'enabled' => false,
'x' => [
'show' => false,
],
'marker' => [
'show' => false,
]
],
'annotations' => [
'xaxis' => [
[
'x' => (time() - 3 * 86400) * 1000,
'strokeDashArray' => 0,
'borderColor' => '#ff0000',
'label' => [
'style' => [
'color' => '#fff',
'background' => "#ff0000"
],
'text' => __('Published')
],
],
]
]
],
]);
echo $this->Bootstrap->card([
'bodyHTML' => $chartActivityHtml,
'bodyClass' => ['p-0'],
'class' => ['h-100'],
]);
?>
</div>
<div class="stat-panel-md2">
<?php
$countsHtml = $this->Bootstrap->render(
'<table class="table align-middle table-borderless mb-0">
<tbody>
<tr>
<td><span class="fw-bold fs-4">{{proposal_count}}</span> <span class="text-uppercase fw-light fs-7">{{proposal_text}}</span></td>
<td><span class="fw-bold fs-4">{{sighting_count}}</span> <span class="text-uppercase fw-light fs-7">{{sighting_text}}</span></td>
</tr>
<tr>
<td><span class="fw-bold fs-4">{{extension_count}}</span> <span class="text-uppercase fw-light fs-7">{{extension_text}}</span></td>
<td><span class="fw-bold fs-4">{{deleted_count}}</span> <span class="text-uppercase fw-light fs-7">{{deleted_text}}</span></td>
</tr>
<tr>
<td><span class="fw-bold fs-4">{{feedhit_count}}</span> <span class="text-uppercase fw-light fs-7">{{feedhit_text}}</span></td>
<td><span class="fw-bold fs-4 {{warninglist_class}}">{{warninglist_count}}</span> <span class="text-uppercase fw-light fs-7">{{warninglist_text}}</span></td>
</tr>
<tr>
<td><span class="fw-bold fs-4">{{relationship_count}}</span> <span class="text-uppercase fw-light fs-7">{{relationship_text}}</span></td>
<td><span class="fw-bold fs-4">{{iocs_count}}</span><span class="fw-light fs-8" title="{{iocs_percentage_title}}">{{iocs_percentage}}</span> <span class="text-uppercase fw-light fs-7">{{ioc_text}}</span></td>
</tr>
</tbody>
</table>',
[
'proposal_text' => __('Proposals'),
'deleted_text' => __('Deleted'),
'eventreport_text' => __('Event Reports'),
'relationship_text' => __('Relationships'),
'correlation_text' => __('Correlations'),
'sighting_text' => __('Sightings'),
'extension_text' => __('Extensions'),
'ioc_text' => __('IoCs'),
'iocs_percentage_title' => __('Percentage of IoCs compared to total amount of attributes'),
'feedhit_text' => __('Feed hits'),
'warninglist_text' => __('Warninglist hits'),
'placeholder2_text' => 'text2',
'proposal_count' => $stats['stat_counts']['proposals'],
'deleted_count' => $stats['stat_counts']['attribute_deleted'],
'eventreport_count' => $stats['stat_counts']['eventreports'],
'relationship_count' => $stats['stat_counts']['relationships'],
'correlation_count' => $stats['stat_counts']['correlations'],
'sighting_count' => $stats['stat_counts']['sightings'],
'extension_count' => $stats['stat_counts']['extensions'],
'iocs_count' => $stats['stat_counts']['iocs'],
'iocs_percentage' => sprintf('%s%%', round(100 * $stats['stat_counts']['iocs'] / $stats['stat_counts']['attributes'])),
'feedhit_count' => $stats['stat_counts']['feed_correlations'],
'warninglist_count' => count($warningslist_hits),
'warninglist_class' => count($warningslist_hits) > 0 ? 'text-danger' : 'text-body',
]
);
echo $this->Bootstrap->card([
'bodyHTML' => $countsHtml,
'bodyClass' => ['d-flex align-items-center py-1'],
'class' => ['h-100'],
]);
?>
</div>
<div class="stat-panel-sm chart-distribution-container">
<?php
echo $this->Bootstrap->card([
'bodyHTML' => $this->element('charts/generic', [
'series' => $stat_distribution,
'chartOptions' => [
'labels' => $stats['distribution_levels'],
'chart' => [
'type' => 'radialBar',
'height' => 190,
],
'plotOptions' => [
'radialBar' => [
'dataLabels' => [
'total' => [
'label' => 'Distribution',
'show' => true,
'fontSize' => 'var(--bs-body-font-size)',
'formatter' => 'totalDistributionFormatter',
]
]
]
],
],
]),
'bodyClass' => ['p-0', 'd-flex flex-wrap align-items-center'],
'class' => ['h-100'],
]);
?>
</div>
<div class="stat-panel-sm">
<?php
$chartOptionsCommon = [
'chart' => [
'type' => 'donut',
'height' => 160,
'offsetY' => 6,
],
'dataLabels' => [
'enabled' => false
],
'legend' => [
'show' => false,
],
'plotOptions' => [
'pie' => [
'donut' => [
'size' => '70%',
'labels' => [
'show' => true,
'value' => [
'offsetY' => 0,
],
'total' => [
'show' => true,
// 'showAlways' => true,
'label' => __('??'),
'fontSize' => 'var(--bs-body-font-size)',
'formatter' => 'alert(1)',
]
]
]
]
],
];
$chartOptionsObjects = $chartOptionsCommon;
$chartOptionsObjects['plotOptions']['pie']['donut']['labels']['total']['label'] = __('Objects');
$chartOptionsObjects['plotOptions']['pie']['donut']['labels']['total']['formatter'] = 'totalObjectFormatter';
echo $this->Bootstrap->card([
'bodyHTML' => $this->element('charts/pie', [
'data' => $stats['stat_objects_6'],
'chartOptions' => $chartOptionsObjects,
]),
'bodyClass' => ['p-0', 'd-flex flex-wrap align-items-center'],
'class' => ['h-100'],
]);
?>
</div>
<div class="stat-panel-sm">
<?php
$chartOptionsAttributes = $chartOptionsCommon;
$chartOptionsAttributes['plotOptions']['pie']['donut']['labels']['total']['label'] = __('Attributes');
$chartOptionsAttributes['plotOptions']['pie']['donut']['labels']['total']['formatter'] = 'totalAttributeFormatter';
echo $this->Bootstrap->card([
'bodyHTML' => $this->element('charts/pie', [
'data' => $stats['stat_attributes_6'],
'chartOptions' => $chartOptionsAttributes
]),
'bodyClass' => ['p-0', 'd-flex flex-wrap align-items-center'],
'class' => ['h-100'],
]);
?>
</div>
<div class="stat-panel-md">
<?php
$chartSightingsHtml = $this->element('charts/generic', [
'series' => [
['data' => array_map(function ($entry) {
return $entry[1];
}, $recent_sightings)]
],
'chartOptions' => [
'chart' => [
'type' => 'bar',
'height' => '160',
'toolbar' => [
'show' => false,
],
'zoom' => [
'enabled' => false,
],
],
'stroke' => [
'curve' => 'straight'
],
'title' => [
'text' => __('Recent sightings'),
],
'labels' => array_map(function ($entry) {
return $entry[0];
}, $recent_sightings),
'grid' => [
'show' => true,
'yaxis' => [
'lines' => [
'show' => true,
],
],
],
'yaxis' => [
'show' => false,
],
'xaxis' => [
'type' => 'datetime',
],
'dataLabels' => [
'enabled' => true,
],
'tooltip' => [
'enabled' => false,
'x' => [
'show' => false,
],
'marker' => [
'show' => false,
]
],
],
]);
echo $this->Bootstrap->card([
'bodyHTML' => $chartSightingsHtml,
'bodyClass' => ['p-0'],
'class' => ['h-100'],
]);
?>
</div>
<div class="stat-panel-md">
<?php
$tmpHtml = '<strong>Relevant correlations go here</strong>';
$tmpHtml .= '<ul class="mt-2">';
$tmpHtml .= ' <li>Events with some context overlap</li>';
$tmpHtml .= ' <li>Events created by other orgs</li>';
$tmpHtml .= ' <li>...</li>';
$tmpHtml .= '</ul>';
echo $this->Bootstrap->card([
'bodyHTML' => $tmpHtml,
'bodyClass' => ['pt-1'],
'class' => ['h-100'],
]);
?>
</div>
</div>
</div>
<script>
const url = "<?= $baseurl ?>/events/getStatistics/<?= $eventid ?>"
function totalObjectFormatter(w) {
return '<?= $stats['stat_counts']['objects'] ?>'
}
function totalAttributeFormatter(w) {
return '<?= $stats['stat_counts']['attributes'] ?>'
}
function totalDistributionFormatter(w) {
return ''
}
</script>
<style>
.stat-container .row>.col-2 {
/* min-width: 270px;
max-width: 320px; */
}
.stat-container .row>div.stat-panel-sm {
height: 160px;
width: 160px;
}
.stat-container .row>div.stat-panel-md {
height: 160px;
width: 280px;
}
.stat-container .row>div.stat-panel-md2 {
height: 160px;
width: 300px;
}
.stat-container .row>div.stat-panel-lg {
height: 160px;
width: 400px;
}
.stat-container .apexcharts-datalabels-group .apexcharts-datalabel-label {
padding: 0 5px;
white-space: nowrap;
paint-order: stroke;
stroke: var(--bs-card-bg);
stroke-width: 2px;
}
.apexcharts-svg text {
fill: var(--bs-body-color);
}
</style>

View File

@ -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',

View File

@ -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]));

View File

@ -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));
}],
['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,
]);

View File

@ -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',

View File

@ -56,7 +56,7 @@ if (!function_exists('getResolvableID')) {
#navbar-scrollspy-setting nav.nav-pills .nav-link.main-group:before {
margin-right: 0.25em;
font-family: 'Font Awesome 5 Free';
font-family: 'Font Awesome 6 Free';
font-weight: 900;
-webkit-font-smoothing: antialiased;
display: inline-block;

View File

@ -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),
]);
}],
],

View File

@ -4,7 +4,6 @@ $chartOptions = $chartOptions ?? [];
$seed = mt_rand();
$chartId = "chart-{$seed}";
$chartData = $chartData ?? [];
$chartSeries = [];
if (!empty($series)) {
$chartSeries = $series;
@ -26,18 +25,24 @@ if (!empty($series)) {
opacity: 0.2,
},
animations: {
enabled: false
enabled: true,
speed: 200,
},
}
},
series: <?= json_encode($chartSeries) ?>,
}
const chartOptions = mergeDeep({}, defaultOptions, passedOptions)
if (chartOptions?.plotOptions?.radialBar?.dataLabels?.total?.formatter) {
chartOptions.plotOptions.radialBar.dataLabels.total.formatter = window[chartOptions.plotOptions.radialBar.dataLabels.total.formatter]
}
new ApexCharts(document.querySelector('#<?= $chartId ?>'), chartOptions).render();
})
</script>
<style>
#<?= $chartId ?> .apexcharts-tooltip-y-group {
#<?= $chartId ?>.apexcharts-tooltip-y-group {
padding: 1px;
}
</style>

View File

@ -1,16 +1,24 @@
<?php
$chartOptions = $chartOptions ?? [];
$seed = mt_rand();
$chartId = "chart-{$seed}";
$firstElement = reset($data);
if (!is_array($firstElement)) { // convert the K-V into list of tuple
$tupleList = [];
foreach ($data as $k => $v) {
$tupleList[] = [$k, $v];
}
$data = $tupleList;
}
$data = $data ?? [];
$series = [];
$labels = [];
$totalValue = 0;
foreach ($data as $combined) {
$combinedValues = array_values($combined);
$label = $combinedValues[0];
$label = strval($combinedValues[0]);
$value = $combinedValues[1];
$labels[] = $label;
$series[] = $value;
@ -33,10 +41,11 @@ foreach ($data as $combined) {
top: 1,
left: 1,
blur: 2,
opacity: 0.2,
opacity: 0.15,
},
animations: {
enabled: false
enabled: true,
speed: 200,
},
},
series: <?= json_encode($series) ?>,
@ -59,15 +68,23 @@ foreach ($data as $combined) {
style: {
fontFamily: 'var(--bs-body-font-family)'
}
},
stroke: {
width: 1
}
}
const chartOptions = mergeDeep({}, defaultOptions, passedOptions)
if (chartOptions?.plotOptions?.pie?.donut?.labels?.total?.formatter) {
chartOptions.plotOptions.pie.donut.labels.total.formatter = window[chartOptions.plotOptions.pie.donut.labels.total.formatter]
}
new ApexCharts(document.querySelector('#<?= $chartId ?>'), chartOptions).render();
})
</script>
<style>
#<?= $chartId ?>.apexcharts-tooltip-y-group {
padding: 1px;
/* padding: 1px; */
}
</style>

View File

@ -1,4 +1,8 @@
<?php
$params['div'] = false;
$params['class'] .= ' form-check-input';
$params['templates'] = [
'nestingLabel' => '{{hidden}}{{input}}<label class="form-check-label" {{attrs}}>{{text}}</label>',
'inputContainer' => '<div class="form-check">{{content}}</div>'
];
echo $this->FormFieldMassage->prepareFormElement($this->Form, $params, $fieldData);
?>

View File

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

View File

@ -4,4 +4,3 @@
$params['class'] .= ' form-control';
}
echo $this->FormFieldMassage->prepareFormElement($this->Form, $params, $fieldData);
?>

View File

@ -0,0 +1,10 @@
<?php
// form-check-inline on the div
$params['class'] .= ' form-check-input';
$params['templates'] = [
'nestingLabel' => '{{input}}<label class="form-check-label" {{attrs}}>{{text}}</label>',
'radioWrapper' => sprintf('<div class="form-check %s">{{label}}</div>', !empty($fieldData['inline']) ? 'form-check-inline' : ''),
];
unset($params['inline']);
echo $this->FormFieldMassage->prepareFormElement($this->Form, $params, $fieldData);
?>

View File

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

View File

@ -1,52 +1,54 @@
<?php
if (is_array($fieldData)) {
$fieldTemplate = 'genericField';
if (!empty($fieldData['type'])) {
if (file_exists(ROOT . '/templates/element/genericElements/Form/Fields/' . $fieldData['type'] . 'Field.php')) {
$fieldTemplate = $fieldData['type'] . 'Field';
}
if (is_array($fieldData)) {
$fieldTemplate = 'genericField';
if (!empty($fieldData['type'])) {
if (file_exists(ROOT . '/templates/element/genericElements/Form/Fields/' . $fieldData['type'] . 'Field.php')) {
$fieldTemplate = $fieldData['type'] . 'Field';
}
if (empty($fieldData['label'])) {
if (!isset($fieldData['label']) || $fieldData['label'] !== false) {
$fieldData['label'] = \Cake\Utility\Inflector::humanize($fieldData['field']);
}
}
if (!empty($fieldDesc[$fieldData['field']])) {
$fieldData['label'] .= $this->element(
'genericElements/Form/formInfo', array(
'field' => $fieldData,
'fieldDesc' => $fieldDesc[$fieldData['field']],
'modelForForm' => $modelForForm
)
);
}
$params = array();
if (!empty($fieldData['class'])) {
if (is_array($fieldData['class'])) {
$class = implode(' ', $fieldData['class']);
} else {
$class = $fieldData['class'];
}
$params['class'] = $class;
} else {
$params['class'] = '';
}
if (empty($fieldData['type']) || ($fieldData['type'] !== 'checkbox' && $fieldData['type'] !== 'radio')) {
$params['class'] .= ' form-control';
}
foreach ($fieldData as $k => $fd) {
if (!isset($simpleFieldWhitelist) || in_array($k, $simpleFieldWhitelist) || strpos($k, 'data-') === 0) {
$params[$k] = $fd;
}
}
$temp = $this->element('genericElements/Form/Fields/' . $fieldTemplate, array(
'fieldData' => $fieldData,
'params' => $params
));
if (!empty($fieldData['hidden'])) {
$temp = '<span class="hidden">' . $temp . '</span>';
}
echo $temp;
} else {
echo $fieldData;
}
if (empty($fieldData['label'])) {
if (!isset($fieldData['label']) || $fieldData['label'] !== false) {
$fieldData['label'] = \Cake\Utility\Inflector::humanize($fieldData['field']);
}
}
$fieldDescription = $fieldData['tooltip'] ?? ($fieldDesc[$fieldData['field']] ?? false);
if (!empty($fieldDescription)) {
$fieldData['tooltip'] = $this->element(
'genericElements/Form/formInfo',
array(
'field' => $fieldData,
'fieldDesc' => $fieldDescription,
'modelForForm' => $modelForForm
)
);
}
$params = array();
if (!empty($fieldData['class'])) {
if (is_array($fieldData['class'])) {
$class = implode(' ', $fieldData['class']);
} else {
$class = $fieldData['class'];
}
$params['class'] = $class;
} else {
$params['class'] = '';
}
if (empty($fieldData['type']) || ($fieldData['type'] !== 'checkbox' && $fieldData['type'] !== 'radio')) {
$params['class'] .= ' form-control';
}
foreach ($fieldData as $k => $fd) {
if (!isset($simpleFieldWhitelist) || in_array($k, $simpleFieldWhitelist) || strpos($k, 'data-') === 0) {
$params[$k] = $fd;
}
}
$temp = $this->element('genericElements/Form/Fields/' . $fieldTemplate, array(
'fieldData' => $fieldData,
'params' => $params
));
if (!empty($fieldData['hidden'])) {
$temp = '<span class="hidden">' . $temp . '</span>';
}
echo $temp;
} else {
echo $fieldData;
}

View File

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

View File

@ -22,9 +22,9 @@
],
[
[
'_open' => true,
'open' => true,
'header' => [
'title' => __('Meta fields')
'text' => __('Meta fields')
],
'body' => $metaTemplateString,
],

View File

@ -15,9 +15,9 @@
],
[
[
'_open' => true,
'open' => true,
'header' => [
'title' => __('Meta fields')
'text' => __('Meta fields')
],
'body' => $metaTemplateString,
],

View File

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

View File

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

View File

@ -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,6 @@ foreach ($statistics['usage'] as $scope => $graphData) {
$pieChart
);
$statPie = $this->Bootstrap->card([
'variant' => 'secondary',
'bodyHTML' => $panelHtml,
'bodyClass' => 'py-1 px-2',
'class' => ['shadow-sm', 'h-100']

View File

@ -11,6 +11,6 @@ if (!empty($statistics['usage'])) {
'statistics' => $statistics,
]);
}
$statisticsHtml = sprintf('<div class="container-fluid"><div class="row gx-2">%s</div></div>', $statisticsHtml);
$statisticsHtml = sprintf('<div class="container-fluid px-0"><div class="row gx-2">%s</div></div>', $statisticsHtml);
echo sprintf('<div class="index-statistic-container">%s</div>', $statisticsHtml);
?>

View File

@ -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,6 @@ $cardContent = sprintf(
);
$card = $this->Bootstrap->card([
'variant' => 'secondary',
'bodyHTML' => $cardContent,
'bodyClass' => 'py-1 px-2',
'class' => ['shadow-sm', 'h-100']

View File

@ -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() {

View File

@ -8,7 +8,7 @@
}
$onClick = sprintf(
'onClick="executePagination(%s, %s);"',
"'" . h($tableRandomValue) . "'",
"'" . h($options['update']) . "'",
"'{{url}}'"
);

View File

@ -0,0 +1,5 @@
<?php
if (!isset($data['requirement']) || $data['requirement']) {
echo $data['html'] ?? 'No HTML passed';
}
?>

View File

@ -3,7 +3,7 @@
if (!isset($data['requirement']) || $data['requirement']) {
if (!empty($data['popover_url'])) {
$onClick = sprintf(
'onClick="openModalForButton%s(this, \'%s\', \'%s\')"',
'openModalForButton%s(this, \'%s\', \'%s\')',
$seed,
h($data['popover_url']),
h(!empty($data['reload_url']) ? $data['reload_url'] : '')
@ -15,15 +15,15 @@
if (!empty($data['onClickParams'])) {
foreach ($data['onClickParams'] as $param) {
if ($param === 'this') {
$onClickParams[] = h($param);
$onClickParams[] = $param;
} else {
$onClickParams[] = '\'' . h($param) . '\'';
$onClickParams[] = '\'' . $param . '\'';
}
}
}
$onClickParams = implode(',', $onClickParams);
$onClick = sprintf(
'onClick = "%s%s"',
'%s%s',
(empty($data['url'])) ? 'event.preventDefault();' : '',
(!empty($data['onClick']) ? sprintf(
'%s(%s)',
@ -33,41 +33,37 @@
);
} else if(!empty($data['url'])) {
$onClick = sprintf(
'onClick = "%s"',
'%s',
sprintf('window.location=\'%s\'', $data['url'])
);
}
}
$dataFields = array();
if (!empty($data['data'])) {
foreach ($data['data'] as $dataKey => $dataValue) {
$dataFields[] = sprintf(
'data-%s="%s"',
h($dataKey),
h($dataValue)
);
}
$btnOptions = $data['button'] ?? [];
if (empty($data['isFilter'])) {
$btnOptions['variant'] = !empty($btnOptions['variant']) ? $btnOptions['variant'] : 'primary';
} else if (empty($data['active'])) {
$btnOptions['variant'] = 'light';
} else {
$btnOptions['variant'] = 'secondary';
}
$dataFields = implode(' ', $dataFields);
echo sprintf(
'<button class="btn %s %s" %s href="%s" %s %s %s %s %s>%s%s%s</button>',
empty($data['class']) ? '' : h($data['class']),
empty($data['isFilter']) ? 'btn-primary' : (empty($data['active']) ? 'btn-light' : 'btn-secondary'), // Change the default class for highlighted/active toggles here
empty($data['id']) ? '' : 'id="' . h($data['id']) . '"',
empty($data['url']) ? '#' : $baseurl . h($data['url']), // prevent default is passed if the url is not set
empty($onClick) ? '' : $onClick, // pass $data['onClick'] for the function name to call and $data['onClickParams'] for the parameter list
empty($dataFields) ? '' : $dataFields,
empty($data['title']) ? '' : sprintf('title="%s"', h($data['title'])),
empty($data['style']) ? '' : sprintf('style="%s"', h($data['style'])),
!empty($data['text']) ? '' : (!empty($data['title']) ? sprintf('aria-label="%s"', h($data['title'])) : ''),
empty($data['fa-icon']) ? '' : sprintf(
'<i class="%s fa-%s"></i> ',
empty($data['fa-source']) ? 'fas' : h($data['fa-source']),
h($data['fa-icon'])
),
empty($data['html']) ? '' : $data['html'], // this has to be sanitised beforehand!
empty($data['text']) ? '' : h($data['text'])
);
$btnOptions['text'] = $data['button']['text'] ?? ($data['text'] ?? '');
if (!empty($onClick)) {
$btnOptions['onclick'] = $onClick;
}
if (!empty($data['html'])) {
$btnOptions['html'] = $data['html'];
}
$btnOptions['attrs'] = array_merge([
'href' => empty($data['url']) ? '#' : $baseurl . h($data['url']),
'style' => $data['style'] ?? '',
'aria-label' => !empty($data['text']) && !empty($data['title']) ? $data['title'] : '',
], $data['attrs'] ?? []);
if (!empty($data['data'])) {
$btnOptions['attrs'][sprintf('data-%s', h($dataKey))] = h($dataValue);
}
echo $this->Bootstrap->button($btnOptions);
}
?>

View File

@ -1,13 +1,14 @@
<?php
$contextArray = [];
foreach ($data['context_filters'] as $filteringContext) {
if(!function_exists("generateFilterLinkConfiguration")) {
function generateFilterLinkConfiguration($filteringContext, $viewContext, $request, $tableRandomValue) {
$filteringContext['filterCondition'] = empty($filteringContext['filterCondition']) ? [] : $filteringContext['filterCondition'];
$urlParams = [
'controller' => $this->request->getParam('controller'),
'controller' => $request->getParam('controller'),
'action' => 'index',
'?' => array_merge($filteringContext['filterCondition'], ['filteringLabel' => $filteringContext['label']])
];
$currentQuery = $this->request->getQuery();
$currentQuery = $request->getQuery();
$filteringLabel = !empty($currentQuery['filteringLabel']) ? $currentQuery['filteringLabel'] : '';
$fakeFilteringLabel = !empty($fakeFilteringLabel) ? $fakeFilteringLabel : false;
unset($currentQuery['page'], $currentQuery['limit'], $currentQuery['sort'], $currentQuery['filteringLabel']);
@ -36,23 +37,87 @@
'onClick' => 'changeIndexContext',
'onClickParams' => [
'this',
$this->Url->build($urlParams, [
$viewContext->Url->build($urlParams, [
'escape' => false, // URL builder escape `&` when multiple ? arguments
]),
"#table-container-${tableRandomValue}",
"#table-container-${tableRandomValue} table.table",
"#table-container-{$tableRandomValue}",
"#table-container-{$tableRandomValue} table.table",
],
'class' => 'btn-sm'
];
if (!empty($filteringContext['viewElement'])) {
$contextItem['html'] = $this->element(
$contextItem['html'] = $viewContext->element(
$filteringContext['viewElement'],
$filteringContext['viewElementParams'] ?? []
);
} else {
$contextItem['text'] = $filteringContext['label'];
}
$contextArray[] = $contextItem;
return $contextItem;
}
}
$contextArray = [];
foreach (($filteringContexts ?? []) as $filteringContext) {
if (!empty($filteringContext['is_group'])) {
$groupHasOneLinkActive = false;
$activeGroupName = null;
$dropdownMenu = [];
foreach ($filteringContext['filters'] as $filteringSubContext) {
$linkContext = generateFilterLinkConfiguration($filteringSubContext, $this, $this->request, $tableRandomValue);
if (!empty($linkContext['onClick']) || empty($linkContext['url'])) {
$onClickParams = [];
if (!empty($linkContext['onClickParams'])) {
$onClickParams = array_map(function($param) {
return $param === 'this' ? $param : sprintf('\'%s\'', $param);
}, $linkContext['onClickParams']);
}
$onClickParams = implode(',', $onClickParams);
$onClick = sprintf(
'%s%s',
(empty($linkContext['url'])) ? 'event.preventDefault();' : '',
(!empty($linkContext['onClick']) ? sprintf(
'%s(%s)',
h($linkContext['onClick']),
$onClickParams
) : '')
);
} else if(!empty($linkContext['url'])) {
$onClick = sprintf(
'%s',
sprintf('window.location=\'%s\'', $linkContext['url'])
);
}
if ($linkContext['active']) {
$groupHasOneLinkActive = true;
$activeGroupName = $filteringSubContext['label'];
}
$dropdownMenu[] = [
'text' => $filteringSubContext['label'],
'icon' => $filteringSubContext['icon'] ?? false,
'variant' => $linkContext['active'] ? 'primary' : '',
'attrs' => [
'onclick' => $onClick,
],
];
}
$dropdownHtml = $this->Bootstrap->dropdownMenu([
'button' => [
'icon' => $filteringContext['icon'] ?? false,
'text' => ($filteringContext['label'] ?? __('Quick Filters')) . ($groupHasOneLinkActive ? sprintf(': %s', $activeGroupName) : ''),
'variant' => $groupHasOneLinkActive ? 'primary' : ($filteringContext['variant'] ?? 'light'),
],
'direction' => 'down',
'menu' => $dropdownMenu,
]);
$contextArray[] = [
'type' => 'raw_html',
'html' => $dropdownHtml,
];
} else {
$contextArray[] = generateFilterLinkConfiguration($filteringContext, $this, $this->request, $tableRandomValue);
}
}
$dataGroup = [

View File

@ -1,18 +1,54 @@
<?php
$dropdownTreshold = 3;
if (!isset($data['requirement']) || $data['requirement']) {
$buttons = '';
foreach ($data['children'] as $child) {
$buttons .= $this->Bootstrap->button([
'variant' => $child['variant'] ?? 'primary',
'text' => $child['text'],
'outline' => !empty($child['outline']),
'icon' => $child['icon'] ?? null,
'params' => array_merge([
'data-onclick-function' => $child['onclick'] ?? '',
$hasHeader = array_filter($data['children'], function($entry) {
return !empty($entry['is-header']);
});
$data['force-dropdown'] = !empty($data['force-dropdown']) ? $data['force-dropdown'] : $hasHeader;
if (!empty($data['force-dropdown']) || count($data['children']) > $dropdownTreshold) {
$menuOptions = [];
foreach ($data['children'] as $child) {
$menuOptions[] = [
'header' => !empty($child['is-header']),
'variant' => $child['variant'] ?? '',
'text' => $child['text'],
'outline' => !empty($child['outline']),
'icon' => $child['icon'] ?? null,
'attrs' => array_merge([
'onclick' => 'multiActionClickHandler(this)',
'data-onclick-function' => $child['onclick'] ?? '',
'data-table-random-value' => $tableRandomValue,
], $child['params'] ?? [])
];
}
$buttons = $this->Bootstrap->dropdownMenu([
'button' => [
'text' => __('Actions'),
'icon' => 'check-square',
'variant' => 'primary',
'class' => [''],
],
'attrs' => [
'data-table-random-value' => $tableRandomValue,
'onclick' => 'multiActionClickHandler(this)'
], $child['params'] ?? [])
],
'menu' => $menuOptions,
]);
} else {
foreach ($data['children'] as $child) {
$buttons .= $this->Bootstrap->button([
'variant' => $child['variant'] ?? 'primary',
'text' => $child['text'],
'outline' => !empty($child['outline']),
'icon' => $child['icon'] ?? null,
'onclick' => 'multiActionClickHandler(this)',
'attrs' => array_merge([
'data-onclick-function' => $child['onclick'] ?? '',
'data-table-random-value' => $tableRandomValue,
], $child['params'] ?? [])
]);
}
}
echo sprintf(
'<div class="multi_select_actions btn-group me-2 flex-wrap collapse" role="group" aria-label="button-group" data-table-random-value="%s">%s</div>',

View File

@ -29,12 +29,10 @@
$numberActiveFilters += count($activeFilters['filteringMetaFields']) - 1;
}
$buttonConfig = [
'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue)),
'icon' => 'filter',
'variant' => $numberActiveFilters > 0 ? 'warning' : 'primary',
'params' => [
'title' => __('Filter index'),
'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue))
]
'title' => __('Filter index'),
];
if (count($activeFilters) > 0) {
$buttonConfig['badge'] = [

View File

@ -19,27 +19,31 @@ $availableColumnsHtml = $this->element('/genericElements/ListTopBar/group_table_
$metaTemplateColumnMenu = [];
if (!empty($meta_templates)) {
$metaTemplateColumnMenu[] = ['header' => true, 'text' => __('Meta Templates'), 'icon' => 'object-group',];
foreach ($meta_templates as $meta_template) {
$numberActiveMetaField = !empty($tableSettings['visible_meta_column'][$meta_template->id]) ? count($tableSettings['visible_meta_column'][$meta_template->id]) : 0;
$metaTemplateColumnMenu[] = [
'text' => $meta_template->name,
'sup' => $meta_template->version,
'badge' => [
'text' => $numberActiveMetaField,
'variant' => 'secondary',
'title' => __n('{0} meta-field active for this meta-template', '{0} meta-fields active for this meta-template', $numberActiveMetaField, $numberActiveMetaField),
],
'keepOpen' => true,
'menu' => [
[
'html' => $this->element('/genericElements/ListTopBar/group_table_action/hiddenMetaColumns', [
'tableSettings' => $tableSettings,
'table_setting_id' => $data['table_setting_id'],
'meta_template' => $meta_template,
])
]
],
];
if (empty($meta_templates_enabled)) {
$metaTemplateColumnMenu[] = ['header' => false, 'text' => __('- No enabled Meta Templates found -'), 'class' => ['disabled', 'muted']];
} else {
foreach ($meta_templates_enabled as $meta_template) {
$numberActiveMetaField = !empty($tableSettings['visible_meta_column'][$meta_template->id]) ? count($tableSettings['visible_meta_column'][$meta_template->id]) : 0;
$metaTemplateColumnMenu[] = [
'text' => $meta_template->name,
'sup' => $meta_template->version,
'badge' => [
'text' => $numberActiveMetaField,
'variant' => 'secondary',
'title' => __n('{0} meta-field active for this meta-template', '{0} meta-fields active for this meta-template', $numberActiveMetaField, $numberActiveMetaField),
],
'keepOpen' => true,
'menu' => [
[
'html' => $this->element('/genericElements/ListTopBar/group_table_action/hiddenMetaColumns', [
'tableSettings' => $tableSettings,
'table_setting_id' => $data['table_setting_id'],
'meta_template' => $meta_template,
])
]
],
];
}
}
}
$indexColumnMenu = array_merge(
@ -63,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'],
],
@ -85,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,
],

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,7 @@
<?php
if (!empty($field['path'])) {
$value = Cake\Utility\Hash::extract($data, $field['path']);
} else {
$value = $data;
}
echo $field['function']($value, $this);

View File

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

View File

@ -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,12 +17,20 @@ 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,
'warning' => $metaField->warning ?? null,
'info' => $metaField->info ?? null,
'danger' => $metaField->danger ?? null
// 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,
'notice_warning' => $metaTemplateField->warning ?? null,
'notice_info' => $metaTemplateField->info ?? null,
'notice_danger' => $metaTemplateField->danger ?? null
];
$labelPrintedOnce = true;
}
@ -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',

View File

@ -0,0 +1,207 @@
<?php
use Cake\Routing\Router;
use Cake\Utility\Inflector;
?>
<div class="action-bar d-flex flex-nowrap flex-row mt-2 mb-1 rounded">
<?php
if (!empty($actions)) {
echo '<div>';
$badgeNumber = 0;
$actionsInMenu = [];
foreach ($actions as $i => $actionEntry) {
if (!empty($actionEntry['url_vars'])) {
$actionEntry['url'] = $this->DataFromPath->buildStringFromDataPath($actionEntry['url'], $entity, $actionEntry['url_vars']);
}
if (!empty($actionEntry['badge'])) {
$badgeNumber += 1;
}
if (!empty($actionEntry['isPOST'])) {
$onclickFunction = sprintf('UI.overlayUntilResolve(this, UI.submissionModalAutoGuess(\'%s\'))', h(Router::url($actionEntry['url'])));
} else if (!empty($actionEntry['isRedirect'])) {
$onclickFunction = sprintf('window.location.replace(\'%s\');', h(Router::url($actionEntry['url'])));
} else {
$onclickFunction = sprintf('UI.overlayUntilResolve(this, UI.modalFromUrl(\'%s\'))', h(Router::url($actionEntry['url'])));
}
$buttonBadge = !empty($actionEntry['badge']) ? $this->Bootstrap->badge($actionEntry['badge']) : '';
$buttonConfig = [
'text' => h($actionEntry['label']),
'icon' => h($actionEntry['icon'] ?? false),
'variant' => $actionEntry['variant'] ?? 'primary',
'class' => ['text-nowrap'],
'size' => 'sm',
'onclick' => $onclickFunction,
'badge' => $buttonBadge,
];
if (!empty($actionEntry['menu'])) {
if (!empty($actionEntry['menu_primary'])) {
$buttonConfig['_menu_primary'] = $actionEntry['menu_primary'];
}
$actionsInMenu[$actionEntry['menu']][] = $buttonConfig;
} else {
echo $this->Bootstrap->button($buttonConfig, $buttonBadge);
}
}
if (!empty($actionsInMenu)) {
foreach ($actionsInMenu as $menuID => $actions) {
$defaultMenuConfig = [
'text' => Inflector::humanize($menuID),
'variant' => 'primary',
'outline' => true,
'size' => 'sm',
];
$actionMenuRootConfig = $actionMenu[$menuID] ?? [];
if (empty($actionMenuRootConfig)) {
$primaryItem = array_filter($actions, function ($action) {
return !empty($action['_menu_primary']);
});
if (!empty($primaryItem)) {
$actionMenuRootConfig = $primaryItem[0];
$actionMenuRootConfig['split'] = true;
}
}
$menuConfig = array_merge($defaultMenuConfig, $actionMenuRootConfig);
$menuConfig['text'] = $menuConfig['label'] ?? $menuConfig['text'];
$actions = array_map(function($action) {
$action['outline'] = true;
return $action;
}, $actions);
echo $this->Bootstrap->dropdownMenu([
'dropdown-class' => '',
'alignment' => 'start',
'direction' => 'down',
'button' => $menuConfig,
'submenu_direction' => 'end',
'attrs' => [],
'menu' => $actions,
]);
}
}
echo '</div>';
}
if (!empty($links)) {
$goToLinks = [];
$linksInMenu = [];
echo '<div class="ms-auto">';
echo '<div class="d-flex gap-1">';
foreach ($links as $i => $linkEntry) {
if (!empty($linkEntry['is-go-to'])) {
if (is_bool($linkEntry['is-go-to'])) {
$goToLinks['_root'][] = $linkEntry;
} else {
$goToLinks[$linkEntry['is-go-to']][] = $linkEntry;
}
continue;
}
if (empty($linkEntry['route_path'])) {
$active = false;
} else {
$active = $linkEntry['route_path'] == $route_path;
}
if (!empty($linkEntry['url_vars'])) {
$linkEntry['url'] = $this->DataFromPath->buildStringFromDataPath($linkEntry['url'], $entity, $linkEntry['url_vars']);
}
if (!empty($linkEntry['selfLink'])) {
$url = Router::url(null);
} else {
$url = Router::url($linkEntry['url']);
}
$buttonConfig = [
'nodeType' => 'a',
'text' => $linkEntry['label'],
'icon' => $linkEntry['icon'],
'badge' => $linkEntry['badge'] ?? false,
'variant' => 'link',
'outline' => $active,
'class' => ['text-nowrap', 'text-decoration-none', 'btn-link-hover-shadow'],
'size' => 'sm',
'attrs' => [
'href' => $url,
],
];
if (!empty($linkEntry['menu'])) {
$linksInMenu[$linkEntry['menu']][] = $buttonConfig;
} else {
echo $this->Bootstrap->button($buttonConfig, $buttonBadge);
}
}
if (!empty($linksInMenu)) {
foreach ($linksInMenu as $menuID => $links) {
$defaultMenuConfig = [
'text' => Inflector::humanize($menuID),
'variant' => 'secondary',
'size' => 'sm',
'outline' => true,
];
$menuConfig = array_merge($defaultMenuConfig, $linkMenu[$menuID] ?? []);
$menuConfig['text'] = $menuConfig['label'] ?: $menuConfig['text'];
$links = array_map(function($link) {
$action['outline'] = true;
return $link;
}, $links);
echo $this->Bootstrap->dropdownMenu([
'dropdown-class' => '',
'alignment' => 'end',
'direction' => 'down',
'button' => $menuConfig,
'submenu_direction' => 'end',
'attrs' => [],
'menu' => $links,
]);
}
}
echo '</div>';
if (!empty($goToLinks)) {
$menu = [];
foreach ($goToLinks as $menuID => $links) {
$jumpToButtons = array_map(function($link) {
$url = Router::url($link['url']);
return [
'nodeType' => 'a',
'text' => h($link['label']),
'variant' => 'link',
'icon' => h($link['icon']),
'class' => ['text-nowrap'],
'attrs' => [
'href' => h($url),
],
];
}, $links);
if ($menuID === '_root') {
$menu = array_merge($menu, $jumpToButtons);
} else {
$subMenuConfig = $goToMenu[$menuID] ?? [];
$subMenu = [
'nodeType' => 'a',
'text' => h($subMenuConfig['label']),
'variant' => h($subMenuConfig['variant'] ?? 'link'),
'icon' => h($subMenuConfig['icon']),
'class' => ['text-nowrap'],
'keepOpen' => true,
'menu' => $jumpToButtons
];
$menu[] = $subMenu;
}
}
echo $this->Bootstrap->dropdownMenu([
'dropdown-class' => '',
'alignment' => 'end',
'direction' => 'down',
'button' => [
'text' => 'Go To',
'variant' => 'secondary',
'icon' => 'location-arrow',
],
'submenu_direction' => 'start',
'attrs' => [],
'menu' => $menu,
]);
}
echo '</div>';
}
?>
</div>

View File

@ -2,7 +2,7 @@
<div class="left-navbar">
<a class="navbar-brand d-sm-block d-none" href="<?= $baseurl ?>">
<div class="composed-app-icon-container">
<span class="app-icon w-100 h-100" title="<?= __('Cerebrate') ?>"></span>
<span class="app-icon w-100 h-100" style="top: 0.2em; position: absolute;"></span>
</div>
</a>
<button class="navbar-toggler d-sm-none" type="button" data-bs-toggle="collapse" data-bs-target="#app-sidebar" aria-controls="app-sidebar" aria-expanded="false" aria-label="Toggle navigation">
@ -13,6 +13,6 @@
<?= $this->element('layouts/header/header-breadcrumb'); ?>
</div>
<div class="right-navbar">
<?= $this->element('layouts/header/header-right'); ?>
<?= $this->element('layouts/header/header-right'); ?>
</div>
</div>
</div>

View File

@ -14,7 +14,7 @@ $this->Breadcrumbs->setTemplates([
'wrapper' => sprintf(
'<nav class="header-breadcrumb d-lg-block d-none"{{attrs}}><ol class="">{{content}}</ol></nav>'
),
'item' => '<li class="header-breadcrumb-item"{{attrs}}><i class="{{icon}} me-1"></i><a class="{{linkClass}}" href="{{url}}"{{innerAttrs}}>{{title}}</a></li>{{separator}}',
'item' => '<li class="header-breadcrumb-item"{{attrs}}><a class="{{linkClass}}" href="{{url}}"{{innerAttrs}}><i class="{{icon}} me-1"></i>{{title}}</a></li>{{separator}}',
'itemWithoutLink' => '<li class="header-breadcrumb-item"{{attrs}}><span{{innerAttrs}}>{{title}}</span></li>{{separator}}',
'separator' => '<li class="header-breadcrumb-separator"{{attrs}}><span{{innerAttrs}}><i class="fa fa-sm fa-angle-right"></i></span></li>'
]);
@ -38,57 +38,11 @@ if (!empty($breadcrumb)) {
$this->Breadcrumbs->add(h($entry['label']), Router::url($entry['url']), [
'title' => h($entry['label']),
'templateVars' => [
'linkClass' => $i == 0 ? 'fw-light' : '',
'linkClass' => 'icon-link icon-link-hover ' . ($i == 0 ? 'fw-light' : ''),
'icon' => ($i == 0 && !empty($entry['icon'])) ? $this->FontAwesome->getClass(h($entry['icon'])) : ''
]
]);
}
$lastCrumb = $breadcrumb[count($breadcrumb) - 1];
if (!empty($lastCrumb['links'])) {
// dd($lastCrumb['links']);
foreach ($lastCrumb['links'] as $i => $linkEntry) {
if (empty($linkEntry['route_path'])) {
$active = false;
} else {
$active = $linkEntry['route_path'] == $lastCrumb['route_path'];
}
if (!empty($linkEntry['url_vars'])) {
$linkEntry['url'] = $this->DataFromPath->buildStringFromDataPath($linkEntry['url'], $entity, $linkEntry['url_vars']);
}
if (!empty($linkEntry['selfLink'])) {
$url = Router::url(null);
} else {
$url = Router::url($linkEntry['url']);
}
$breadcrumbLinks .= sprintf(
'<a class="btn btn-%s btn-sm text-nowrap" role="button" href="%s">%s</a>',
$active ? 'secondary' : 'outline-secondary',
$url,
h($linkEntry['label'])
);
}
}
$badgeNumber = 0;
if (!empty($lastCrumb['actions'])) {
foreach ($lastCrumb['actions'] as $i => $actionEntry) {
if (!empty($actionEntry['url_vars'])) {
$actionEntry['url'] = $this->DataFromPath->buildStringFromDataPath($actionEntry['url'], $entity, $actionEntry['url_vars']);
}
if (!empty($actionEntry['badge'])) {
$badgeNumber += 1;
}
$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']))),
!empty($actionEntry['icon']) ? $this->FontAwesome->getClass(h($actionEntry['icon'])) : '',
h($actionEntry['label']),
!empty($actionEntry['badge']) ? $this->Bootstrap->badge($actionEntry['badge']) : ''
);
}
}
}
?>
@ -98,28 +52,23 @@ echo $this->Breadcrumbs->render(
[],
['separator' => '']
);
// $actionBar = '<div class="alert alert-primary">test</div>';
// $this->assign('actionBar', $actionBar);
?>
<?php if (!empty($breadcrumbLinks) || !empty($breadcrumbAction)) : ?>
<div class="breadcrumb-link-container position-absolute end-0 d-flex">
<div class="header-breadcrumb-children d-none d-md-flex btn-group">
<?= $breadcrumbLinks ?>
<?php if (!empty($breadcrumbAction)) : ?>
<a class="btn btn-primary btn-sm dropdown-toggle" href="#" role="button" id="dropdownMenuBreadcrumbAction" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<?= __('Actions') ?>
<?=
$badgeNumber == 0 ? '' : $this->Bootstrap->badge([
'text' => h($badgeNumber),
'variant' => 'warning',
'pill' => false,
'title' => __n('There is {0} action available', 'There are {0} actions available', $badgeNumber, h($badgeNumber)),
])
?>
</a>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="dropdownMenuBreadcrumbAction">
<?= $breadcrumbAction ?>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php $this->start('actionBar'); ?>
<?php // if (!empty($breadcrumbLinks) || !empty($breadcrumbAction)) : ?>
<?php
$lastCrumb = $breadcrumb[count($breadcrumb) - 1];
echo $this->element('layouts/action-bar', [
'links' => $lastCrumb['links'] ?? [],
'actions' => $lastCrumb['actions'] ?? [],
'route_path' => $lastCrumb['route_path'] ?? '',
'goToMenu' => $lastCrumb['goToMenu'] ?? [],
'linkMenu' => $lastCrumb['linkMenu'] ?? [],
'actionMenu' => $lastCrumb['actionMenu'] ?? [],
]);
?>
<?php // endif; ?>
<?php $this->end(); ?>

View File

@ -25,6 +25,7 @@ $variant = array_flip($severity)[$maxSeverity];
if ($hasNotification) {
echo $this->Bootstrap->notificationBubble([
'variant' => $variant,
'borderVariant' => 'light',
]);
}
?>

View File

@ -12,10 +12,13 @@ use Cake\Routing\Router;
<div class="fw-light"><?= __('Logged in as') ?></div>
<div>
<?= $this->SocialProvider->getIcon($this->request->getAttribute('identity')) ?>
<span class="ms-1 me-3">
[<?= h($loggedUser['Organisation']['name']) ?>]
<div class="ms-1 me-3">
<strong><?= h($this->request->getAttribute('identity')['email']) ?></strong>
</span>
</div>
<div class="ms-1 me-3 d-flex">
[<?= h($loggedUser['Organisation']['name']) ?>]
<span class="ms-auto"><?= h($loggedUser['Role']['name']) ?></span>
</div>
</div>
</h6>
<div class="dropdown-divider"></div>

View File

@ -4,7 +4,7 @@
<div class="sidebar-wrapper d-flex flex-column">
<div class="sidebar-scroll">
<div class="sidebar-content">
<ul class="sidebar-elements">
<ul id="sidebar-elements" class="sidebar-elements">
<?php foreach ($menu as $category => $categorized) : ?>
<?php if ($category == '__bookmarks') : ?>
<?php if ($bookmarkIncluded) : ?>

View File

@ -6,7 +6,5 @@ echo $this->Bootstrap->button([
'variant' => 'primary',
'size' => 'sm',
'class' => 'mb-1',
'params' => [
'id' => 'btn-add-bookmark',
]
'id' => 'btn-add-bookmark',
]);

View File

@ -28,7 +28,7 @@
'size' => 'sm',
'icon' => h($icon),
'class' => ['mb-1', !$validURI ? 'disabled' : ''],
'params' => [
'attrs' => [
'href' => $validURI ? h($url) : '#',
]
]);

Some files were not shown because too many files have changed in this diff Show More