chg: [instance:settings] Improved UI interface

Added searches and notices for individual settings
pull/70/head
mokaddem 2021-07-21 11:18:06 +02:00
parent d501969c1d
commit 9f4fbf0410
9 changed files with 204 additions and 70 deletions

View File

@ -811,6 +811,10 @@ class ACLComponent extends Component
__('Instance'), __('Instance'),
'url' => '/instance/home', 'url' => '/instance/home',
'children' => [ 'children' => [
'settings' => [
'url' => '/instance/settings',
'label' => __('Settings')
],
'migration' => [ 'migration' => [
'url' => '/instance/migrationIndex', 'url' => '/instance/migrationIndex',
'label' => __('Database migration') 'label' => __('Database migration')

View File

@ -108,6 +108,7 @@ class InstanceController extends AppController
$all = $this->Settings->getSettings(true); $all = $this->Settings->getSettings(true);
$this->set('settingsProvider', $all['settingsProvider']); $this->set('settingsProvider', $all['settingsProvider']);
$this->set('settings', $all['settings']); $this->set('settings', $all['settings']);
$this->set('settingsFlattened', $all['settingsFlattened']);
$this->set('notices', $all['notices']); $this->set('notices', $all['notices']);
} }
} }

View File

@ -44,20 +44,37 @@ class SettingsProviderTable extends AppTable
* @param array $settings the settings * @param array $settings the settings
* @return void * @return void
*/ */
private function mergeSettingsIntoSettingConfiguration(array $settingConf, array $settings): array private function mergeSettingsIntoSettingConfiguration(array $settingConf, array $settings, string $path=''): array
{ {
foreach ($settingConf as $key => $value) { foreach ($settingConf as $key => $value) {
if ($this->isLeaf($value)) { if ($this->isLeaf($value)) {
if (isset($settings[$key])) { if (isset($settings[$key])) {
$settingConf[$key]['value'] = $settings[$key]; $settingConf[$key]['value'] = $settings[$key];
} }
if (empty($settingConf[$key]['severity'])) {
$settingConf[$key]['severity'] = 'warning';
}
$settingConf[$key] = $this->evaluateLeaf($settingConf[$key], $settingConf); $settingConf[$key] = $this->evaluateLeaf($settingConf[$key], $settingConf);
$settingConf[$key]['setting-path'] = $path;
} else { } else {
$settingConf[$key] = $this->mergeSettingsIntoSettingConfiguration($value, $settings); $currentPath = empty($path) ? $key : sprintf('%s.%s', $path, $key);
$settingConf[$key] = $this->mergeSettingsIntoSettingConfiguration($value, $settings, $currentPath);
} }
} }
return $settingConf; return $settingConf;
} }
public function flattenSettingsConfiguration(array $settingsProvider, $flattenedSettings=[]): array
{
foreach ($settingsProvider as $key => $value) {
if ($this->isLeaf($value)) {
$flattenedSettings[$key] = $value;
} else {
$flattenedSettings = $this->flattenSettingsConfiguration($value, $flattenedSettings);
}
}
return $flattenedSettings;
}
/** /**
* getNoticesFromSettingsConfiguration Summarize the validation errors * getNoticesFromSettingsConfiguration Summarize the validation errors
@ -175,14 +192,14 @@ class SettingsProviderTable extends AppTable
'description' => 'to del', 'description' => 'to del',
'errorMessage' => 'to del', 'errorMessage' => 'to del',
'default' => '', 'default' => '',
'name' => 'To DEL', 'name' => 'To DEL 2',
'type' => 'string' 'type' => 'string'
], ],
'to-del3' => [ 'to-del3' => [
'description' => 'to del', 'description' => 'to del',
'errorMessage' => 'to del', 'errorMessage' => 'to del',
'default' => '', 'default' => '',
'name' => 'To DEL', 'name' => 'To DEL 2',
'type' => 'string' 'type' => 'string'
], ],
], ],
@ -227,36 +244,6 @@ class SettingsProviderTable extends AppTable
'type' => 'string', 'type' => 'string',
], ],
], ],
'Proxy2' => [
'host' => [
'description' => __('The hostname of an HTTP proxy for outgoing sync requests. Leave empty to not use a proxy.'),
'default' => '',
'name' => __('Host'),
'test' => 'testHostname',
'type' => 'string',
],
'port' => [
'description' => __('The TCP port for the HTTP proxy.'),
'default' => '',
'name' => __('Port'),
'test' => 'testForRangeXY',
'type' => 'integer',
],
'user' => [
'description' => __('The authentication username for the HTTP proxy.'),
'default' => '',
'name' => __('User'),
'test' => 'testEmptyBecomesDefault',
'type' => 'string',
],
'password' => [
'description' => __('The authentication password for the HTTP proxy.'),
'default' => '',
'name' => __('Password'),
'test' => 'testEmptyBecomesDefault',
'type' => 'string',
],
],
], ],
'UI' => [ 'UI' => [
'app.ui.dark' => [ 'app.ui.dark' => [
@ -268,6 +255,40 @@ class SettingsProviderTable extends AppTable
], ],
], ],
'Security' => [ 'Security' => [
'Network' => [
'Proxy Test' => [
'proxy-test.host' => [
'description' => __('The hostname of an HTTP proxy for outgoing sync requests. Leave empty to not use a proxy.'),
'default' => '',
'name' => __('Host'),
'test' => 'testHostname',
'type' => 'string',
],
'proxy-test.port' => [
'description' => __('The TCP port for the HTTP proxy.'),
'default' => '',
'name' => __('Port'),
'test' => 'testForRangeXY',
'type' => 'integer',
],
'proxy-test.user' => [
'description' => __('The authentication username for the HTTP proxy.'),
'default' => '',
'dependsOn' => 'host',
'name' => __('User'),
'test' => 'testEmptyBecomesDefault',
'type' => 'string',
],
'proxy-test.password' => [
'description' => __('The authentication password for the HTTP proxy.'),
'default' => '',
'dependsOn' => 'host',
'name' => __('Password'),
'test' => 'testEmptyBecomesDefault',
'type' => 'string',
],
],
]
], ],
'Features' => [ 'Features' => [
], ],

View File

@ -23,10 +23,12 @@ class SettingsTable extends AppTable
return $settings; return $settings;
} else { } else {
$settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settings); $settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settings);
$settingsFlattened = $this->SettingsProvider->flattenSettingsConfiguration($settingsProvider);
$notices = $this->SettingsProvider->getNoticesFromSettingsConfiguration($settingsProvider, $settings); $notices = $this->SettingsProvider->getNoticesFromSettingsConfiguration($settingsProvider, $settings);
return [ return [
'settings' => $settings, 'settings' => $settings,
'settingsProvider' => $settingsProvider, 'settingsProvider' => $settingsProvider,
'settingsFlattened' => $settingsFlattened,
'notices' => $notices, 'notices' => $notices,
]; ];
} }

View File

@ -17,15 +17,20 @@ $headingPerLevel = [
'warning' => __('Warning settings'), 'warning' => __('Warning settings'),
'info' => __('Info settings'), 'info' => __('Info settings'),
]; ];
$variantFromSeverity = [
'critical' => 'danger',
'warning' => 'warning',
'info' => 'info',
];
$this->set('variantFromSeverity', $variantFromSeverity);
$settingTable = genLevel0($settingsProvider, $this);
$alertVariant = 'info'; $alertVariant = 'info';
$alertBody = ''; $alertBody = '';
$skipHeading = false; $skipHeading = false;
$tableItems = []; $tableItems = [];
foreach (array_keys($mainNoticeHeading) as $level) { foreach (array_keys($mainNoticeHeading) as $level) {
if(!empty($notices[$level])) { if(!empty($notices[$level])) {
$variant = $level == 'critical' ? 'danger' : $level; $variant = $variantFromSeverity[$level];
if (!$skipHeading) { if (!$skipHeading) {
$alertBody .= sprintf('<h5 class="alert-heading">%s</h5>', $mainNoticeHeading[$level]); $alertBody .= sprintf('<h5 class="alert-heading">%s</h5>', $mainNoticeHeading[$level]);
$alertVariant = $variant; $alertVariant = $variant;
@ -60,16 +65,17 @@ $alertBody .= $this->Bootstrap->table([
'items' => $tableItems, 'items' => $tableItems,
]); ]);
$settingNotice = $this->Bootstrap->alert([ $settingNotice = $this->Bootstrap->alert([
'dismissible' => false,
'variant' => $alertVariant, 'variant' => $alertVariant,
'html' => $alertBody 'html' => $alertBody
]); ]);
$settingNotice = sprintf('<div class="mt-3">%s</div>', $settingNotice);
$this->set('settingNotice', $settingNotice);
$settingTable = genLevel0($settingsProvider, $this);
?> ?>
<div class="px-5"> <div class="px-5">
<div class="">
<?= $settingNotice ?>
</div>
<div class="mb-3"> <div class="mb-3">
<input class="form-control" type="text" id="search" placeholder="<?= __('Search settings') ?>" aria-describedby="<?= __('Search setting input') ?>"> <select id="search-settings" class="d-block w-100" aria-describedby="<?= __('Search setting input') ?>"><option></option></select>
</div> </div>
<?= $settingTable; ?> <?= $settingTable; ?>
</div> </div>
@ -86,13 +92,15 @@ function genLevel0($settingsProvider, $appView)
$content0[] = __('No Settings available yet'); $content0[] = __('No Settings available yet');
} }
} }
array_unshift($level0, __('Setting Diagnostic'));
array_unshift($content0, $appView->get('settingNotice'));
$tabsOptions0 = [ $tabsOptions0 = [
// 'vertical' => true, // 'vertical' => true,
// 'vertical-size' => 2, // 'vertical-size' => 2,
'card' => false, 'card' => false,
'pills' => false, 'pills' => false,
'justify' => 'center', 'justify' => 'center',
'content-class' => [''], 'nav-class' => ['settings-tabs'],
'data' => [ 'data' => [
'navs' => $level0, 'navs' => $level0,
'content' => $content0 'content' => $content0
@ -124,7 +132,7 @@ function genLevel1($level1Setting, $appView)
$mainPanelHeight = 'calc(100vh - 8px - 42px - 1rem - 56px - 38px - 1rem)'; $mainPanelHeight = 'calc(100vh - 8px - 42px - 1rem - 56px - 38px - 1rem)';
$container = '<div class="d-flex">'; $container = '<div class="d-flex">';
$container .= "<div class=\"\" style=\"flex: 0 0 10em;\">{$scrollspyNav}</div>"; $container .= "<div class=\"\" style=\"flex: 0 0 10em;\">{$scrollspyNav}</div>";
$container .= "<div data-spy=\"scroll\" data-target=\"#navbar-scrollspy-setting\" data-offset=\"24\" style=\"height: {$mainPanelHeight}\" class=\"p-3 overflow-auto position-relative flex-grow-1\">{$contentHtml}</div>"; $container .= "<div data-spy=\"scroll\" data-target=\"#navbar-scrollspy-setting\" data-offset=\"25\" style=\"height: {$mainPanelHeight}\" class=\"p-3 overflow-auto position-relative flex-grow-1\">{$contentHtml}</div>";
$container .= '</div>'; $container .= '</div>';
return $container; return $container;
} }
@ -151,12 +159,28 @@ function genLevel3($level2Name, $settingGroupName, $setting, $appView)
} else { } else {
$tmpID = sprintf('sp-%s-%s', h($level2Name), h($settingGroupName)); $tmpID = sprintf('sp-%s-%s', h($level2Name), h($settingGroupName));
$settingGroup .= sprintf('<h4 id="%s"><a class="text-reset text-decoration-none" href="#%s">%s</a></h4>', $tmpID, $tmpID, h($settingGroupName)); $settingGroup .= sprintf('<h4 id="%s"><a class="text-reset text-decoration-none" href="#%s">%s</a></h4>', $tmpID, $tmpID, h($settingGroupName));
$groupIssueSeverity = false;
foreach ($setting as $singleSettingName => $singleSetting) { foreach ($setting as $singleSettingName => $singleSetting) {
$tmp = genSingleSetting($singleSettingName, $singleSetting, $appView); $tmp = genSingleSetting($singleSettingName, $singleSetting, $appView);
$settingGroup .= sprintf('<div class="ml-3">%s</div>', $tmp); $settingGroup .= sprintf('<div class="ml-3">%s</div>', $tmp);
if (!empty($singleSetting['error'])) {
$settingVariant = $appView->get('variantFromSeverity')[$singleSetting['severity']];
if ($groupIssueSeverity != 'danger') {
if ($groupIssueSeverity != 'warning') {
$groupIssueSeverity = $settingVariant;
}
}
}
} }
$settingGroup = $appView->Bootstrap->genNode('div', [ $settingGroup = $appView->Bootstrap->genNode('div', [
'class' => ['shadow', 'p-2', 'mb-4', 'rounded', ($appView->get('darkMode') ? 'bg-dark' : 'bg-light')], 'class' => [
'shadow',
'p-2',
'mb-4',
'rounded',
(!empty($groupIssueSeverity) ? "callout callout-${groupIssueSeverity}" : ''),
($appView->get('darkMode') ? 'bg-dark' : 'bg-light')
],
], $settingGroup); ], $settingGroup);
} }
return $settingGroup; return $settingGroup;
@ -175,23 +199,22 @@ function genSingleSetting($settingName, $setting, $appView)
'title' => __('This setting depends on the validity of: {0}', h($setting['dependsOn'])) 'title' => __('This setting depends on the validity of: {0}', h($setting['dependsOn']))
])); ]));
} }
$settingId = str_replace('.', '_', $settingName);
$label = $appView->Bootstrap->genNode('label', [ $label = $appView->Bootstrap->genNode('label', [
'class' => ['font-weight-bolder', 'mb-0'], 'class' => ['font-weight-bolder', 'mb-0'],
'for' => $settingName 'for' => $settingId
], h($setting['name']) . $dependsOnHtml); ], h($setting['name']) . $dependsOnHtml);
$description = ''; $description = '';
if (!empty($setting['description'])) { if (!empty($setting['description'])) {
$description = $appView->Bootstrap->genNode('small', [ $description = $appView->Bootstrap->genNode('small', [
'class' => ['form-text', 'text-muted', 'mt-0'], 'class' => ['form-text', 'text-muted', 'mt-0'],
'id' => "{$settingName}Help" 'id' => "{$settingId}Help"
], h($setting['description'])); ], h($setting['description']));
} }
$error = ''; $error = '';
if (!empty($setting['error'])) { if (!empty($setting['error'])) {
$textColor = ''; $textColor = '';
if ($setting['severity'] != 'critical') { $textColor = "text-{$appView->get('variantFromSeverity')[$setting['severity']]}";
$textColor = "text-{$setting['severity']}";
}
$error = $appView->Bootstrap->genNode('div', [ $error = $appView->Bootstrap->genNode('div', [
'class' => ['d-block', 'invalid-feedback', $textColor], 'class' => ['d-block', 'invalid-feedback', $textColor],
], h($setting['errorMessage'])); ], h($setting['errorMessage']));
@ -200,18 +223,18 @@ function genSingleSetting($settingName, $setting, $appView)
$setting['type'] = 'string'; $setting['type'] = 'string';
} }
if ($setting['type'] == 'string') { if ($setting['type'] == 'string') {
$input = genInputString($settingName, $setting, $appView); $input = genInputString($settingId, $setting, $appView);
} elseif ($setting['type'] == 'boolean') { } elseif ($setting['type'] == 'boolean') {
$input = genInputCheckbox($settingName, $setting, $appView); $input = genInputCheckbox($settingId, $setting, $appView);
$description = ''; $description = '';
} elseif ($setting['type'] == 'integer') { } elseif ($setting['type'] == 'integer') {
$input = genInputInteger($settingName, $setting, $appView); $input = genInputInteger($settingId, $setting, $appView);
} elseif ($setting['type'] == 'select') { } elseif ($setting['type'] == 'select') {
$input = genInputSelect($settingName, $setting, $appView); $input = genInputSelect($settingId, $setting, $appView);
} elseif ($setting['type'] == 'multi-select') { } elseif ($setting['type'] == 'multi-select') {
$input = genInputMultiSelect($settingName, $setting, $appView); $input = genInputMultiSelect($settingId, $setting, $appView);
} else { } else {
$input = genInputString($settingName, $setting, $appView); $input = genInputString($settingId, $setting, $appView);
} }
$container = $appView->Bootstrap->genNode('div', [ $container = $appView->Bootstrap->genNode('div', [
'class' => ['form-group', 'mb-2'] 'class' => ['form-group', 'mb-2']
@ -219,23 +242,24 @@ function genSingleSetting($settingName, $setting, $appView)
return $container; return $container;
} }
function genInputString($settingName, $setting, $appView) function genInputString($settingId, $setting, $appView)
{ {
// debug($setting);
return $appView->Bootstrap->genNode('input', [ return $appView->Bootstrap->genNode('input', [
'class' => [ 'class' => [
'form-control', 'form-control',
"xxx-{$appView->get('variantFromSeverity')[$setting['severity']]} yyy-{$setting['severity']}",
(!empty($setting['error']) ? 'is-invalid' : ''), (!empty($setting['error']) ? 'is-invalid' : ''),
(!empty($setting['error']) ? ($setting['severity'] != 'critical' ? "border-{$setting['severity']} warning" : '') : ''), (!empty($setting['error']) ? "border-{$appView->get('variantFromSeverity')[$setting['severity']]}" : ''),
(!empty($setting['error']) && $setting['severity'] == 'warning' ? 'warning' : ''),
], ],
'type' => 'text', 'type' => 'text',
'id' => $settingName, 'id' => $settingId,
'value' => isset($setting['value']) ? $setting['value'] : "", 'value' => isset($setting['value']) ? $setting['value'] : "",
'placeholder' => $setting['default'] ?? '', 'placeholder' => $setting['default'] ?? '',
'aria-describedby' => "{$settingName}Help" 'aria-describedby' => "{$settingId}Help"
]); ]);
} }
function genInputCheckbox($settingName, $setting, $appView) function genInputCheckbox($settingId, $setting, $appView)
{ {
$switch = $appView->Bootstrap->genNode('input', [ $switch = $appView->Bootstrap->genNode('input', [
'class' => [ 'class' => [
@ -244,13 +268,13 @@ function genInputCheckbox($settingName, $setting, $appView)
'type' => 'checkbox', 'type' => 'checkbox',
'value' => !empty($setting['value']) ? 1 : 0, 'value' => !empty($setting['value']) ? 1 : 0,
'checked' => !empty($setting['value']) ? 'checked' : '', 'checked' => !empty($setting['value']) ? 'checked' : '',
'id' => $settingName, 'id' => $settingId,
]); ]);
$label = $appView->Bootstrap->genNode('label', [ $label = $appView->Bootstrap->genNode('label', [
'class' => [ 'class' => [
'custom-control-label' 'custom-control-label'
], ],
'for' => $settingName, 'for' => $settingId,
], h($setting['description'])); ], h($setting['description']));
$container = $appView->Bootstrap->genNode('div', [ $container = $appView->Bootstrap->genNode('div', [
'class' => [ 'class' => [
@ -260,23 +284,23 @@ function genInputCheckbox($settingName, $setting, $appView)
], implode('', [$switch, $label])); ], implode('', [$switch, $label]));
return $container; return $container;
} }
function genInputInteger($settingName, $setting, $appView) function genInputInteger($settingId, $setting, $appView)
{ {
return $appView->Bootstrap->genNode('input', [ return $appView->Bootstrap->genNode('input', [
'class' => [ 'class' => [
'form-control' 'form-control'
], ],
'params' => [ 'type' => 'number',
'type' => 'integer', 'min' => '0',
'id' => $settingName, 'step' => 1,
'aria-describedby' => "{$settingName}Help" 'id' => $settingId,
] 'aria-describedby' => "{$settingId}Help"
]); ]);
} }
function genInputSelect($settingName, $setting, $appView) function genInputSelect($settingId, $setting, $appView)
{ {
} }
function genInputMultiSelect($settingName, $setting, $appView) function genInputMultiSelect($settingId, $setting, $appView)
{ {
} }
@ -304,7 +328,21 @@ function isLeaf($setting)
?> ?>
<script> <script>
const settingsFlattened = <?= json_encode($settingsFlattened) ?>;
let selectData = []
for (const settingName in settingsFlattened) {
if (Object.hasOwnProperty.call(settingsFlattened, settingName)) {
const setting = settingsFlattened[settingName];
const selectID = settingName.replaceAll('.', '_')
selectData.push({
id: selectID,
text: setting.name,
setting: setting
})
}
}
$(document).ready(function() { $(document).ready(function() {
$('[data-spy="scroll"]').on('activate.bs.scrollspy', function(evt, {relatedTarget}) { $('[data-spy="scroll"]').on('activate.bs.scrollspy', function(evt, {relatedTarget}) {
const $associatedLink = $(`#navbar-scrollspy-setting nav.nav-pills .nav-link[href="${relatedTarget}"]`) const $associatedLink = $(`#navbar-scrollspy-setting nav.nav-pills .nav-link[href="${relatedTarget}"]`)
@ -318,8 +356,62 @@ function isLeaf($setting)
$allNavs.removeClass('group-active').hide() $allNavs.removeClass('group-active').hide()
$associatedNav.addClass('group-active').show() $associatedNav.addClass('group-active').show()
}) })
$('.settings-tabs a[data-toggle="tab"]').on('shown.bs.tab', function (event) {
$('[data-spy="scroll"]').trigger('scroll.bs.scrollspy')
})
$("#search-settings").select2({
data: selectData,
placeholder: '<?= __('Search setting by typing here...') ?>',
templateResult: formatSettingSearchResult,
templateSelection: formatSettingSearchSelection,
})
.on('select2:select', function (e) {
const selected = e.params.data
const settingPath = selected.setting['setting-path']
const settingPathTokenized = settingPath.split('.')
const tabName = settingPathTokenized[0]
const IDtoFocus = 'sp-' + settingPathTokenized.slice(1).join('-')
const $navController = $('.settings-tabs').find('a.nav-link').filter(function() {
return $(this).text() == tabName
})
if ($navController.length == 1) {
$toFocus = $(`#${IDtoFocus}`).parent()
if ($navController.hasClass('active')) {
$toFocus[0].scrollIntoView()
$toFocus.find(`input#${selected.id}`).focus()
} else {
$navController.on('shown.bs.tab.after-selection', () => {
$toFocus[0].scrollIntoView()
$toFocus.find(`input#${selected.id}`).focus()
$navController.off('shown.bs.tab.after-selection')
}).tab('show')
}
}
$("#search-settings").val(null).trigger('change.select2');
})
}) })
function formatSettingSearchResult(state) {
if (!state.id) {
return state.text;
}
const $state = $('<div/>').append(
$('<div/>').addClass('d-flex justify-content-between')
.append(
$('<span/>').addClass('font-weight-bold').text(state.text),
$('<span/>').addClass('font-weight-light').text(state.setting['setting-path'].replaceAll('.', ' ▸ '))
),
$('<div/>').addClass('font-italic font-weight-light ml-3').text(state.setting['description'])
)
return $state
}
function formatSettingSearchSelection(state) {
return state.text
}
</script> </script>
<style> <style>
@ -353,4 +445,9 @@ function isLeaf($setting)
#navbar-scrollspy-setting nav.nav-pills .nav-link.main-group:before { #navbar-scrollspy-setting nav.nav-pills .nav-link.main-group:before {
content: "\f0da"; content: "\f0da";
} }
.select2-container {
max-width: 100%;
min-width: 100%;
}
</style> </style>

View File

@ -41,6 +41,7 @@ $cakeDescription = 'Cerebrate';
<?= $this->Html->script('main.js') ?> <?= $this->Html->script('main.js') ?>
<?= $this->Html->script('bootstrap-helper.js') ?> <?= $this->Html->script('bootstrap-helper.js') ?>
<?= $this->Html->script('api-helper.js') ?> <?= $this->Html->script('api-helper.js') ?>
<?= $this->Html->script('select2.min.js') ?>
<?= $this->Html->script('CodeMirror/codemirror.js') ?> <?= $this->Html->script('CodeMirror/codemirror.js') ?>
<?= $this->Html->script('CodeMirror/mode/javascript/javascript') ?> <?= $this->Html->script('CodeMirror/mode/javascript/javascript') ?>
<?= $this->Html->script('CodeMirror/addon/hint/show-hint') ?> <?= $this->Html->script('CodeMirror/addon/hint/show-hint') ?>
@ -53,6 +54,8 @@ $cakeDescription = 'Cerebrate';
<?= $this->Html->css('CodeMirror/codemirror-additional') ?> <?= $this->Html->css('CodeMirror/codemirror-additional') ?>
<?= $this->Html->css('CodeMirror/addon/hint/show-hint') ?> <?= $this->Html->css('CodeMirror/addon/hint/show-hint') ?>
<?= $this->Html->css('CodeMirror/addon/lint/lint') ?> <?= $this->Html->css('CodeMirror/addon/lint/lint') ?>
<?= $this->Html->css('select2.min') ?>
<?= $this->Html->css('select2-bootstrap4.min') ?>
<?= $this->fetch('meta') ?> <?= $this->fetch('meta') ?>
<?= $this->fetch('css') ?> <?= $this->fetch('css') ?>
<?= $this->fetch('script') ?> <?= $this->fetch('script') ?>
@ -83,5 +86,6 @@ $cakeDescription = 'Cerebrate';
</body> </body>
<script> <script>
const darkMode = (<?= empty($darkMode) ? 'false' : 'true' ?>) const darkMode = (<?= empty($darkMode) ? 'false' : 'true' ?>)
$.fn.select2.defaults.set('theme', 'bootstrap4');
</script> </script>
</html> </html>

File diff suppressed because one or more lines are too long

1
webroot/css/select2.min.css vendored Normal file

File diff suppressed because one or more lines are too long

2
webroot/js/select2.min.js vendored Normal file

File diff suppressed because one or more lines are too long