diff --git a/src/Model/Table/SettingsProviderTable.php b/src/Model/Table/SettingsProviderTable.php index e0cdc9f..08ea323 100644 --- a/src/Model/Table/SettingsProviderTable.php +++ b/src/Model/Table/SettingsProviderTable.php @@ -248,12 +248,15 @@ class SettingsProviderTable extends AppTable ], 'UI' => [ 'General' => [ - ], - 'app.ui.dark' => [ - 'description' => __('Enable the dark theme of the application'), - 'default' => false, - 'name' => __('Dark theme'), - 'type' => 'boolean', + 'app.ui.dark' => [ + 'description' => __('Enable the dark theme of the application'), + 'default' => false, + 'name' => __('Dark theme'), + 'test' => function() { + return 'Fake error'; + }, + 'type' => 'boolean', + ], ], ], ], diff --git a/templates/Instance/settings.php b/templates/Instance/settings.php index ca8a639..44327ff 100644 --- a/templates/Instance/settings.php +++ b/templates/Instance/settings.php @@ -129,7 +129,7 @@ function genLevel1($level1Setting, $appView) } $contentHtml = implode('', $content1); $scrollspyNav = genScrollspyNav($nav1); - $mainPanelHeight = 'calc(100vh - 8px - 42px - 1rem - 56px - 38px - 1rem)'; + $mainPanelHeight = 'calc(100vh - 42px - 1rem - 56px - 38px - 1rem)'; $container = '
'; $container .= "
{$scrollspyNav}
"; $container .= "
{$contentHtml}
"; @@ -285,11 +285,13 @@ function genInputCheckbox($settingName, $setting, $appView) $settingId = str_replace('.', '_', $settingName); $switch = $appView->Bootstrap->genNode('input', [ 'class' => [ - 'custom-control-input' + 'custom-control-input', + (!empty($setting['error']) ? 'is-invalid' : ''), + (!empty($setting['error']) && $setting['severity'] == 'warning' ? 'warning' : ''), ], 'type' => 'checkbox', 'value' => !empty($setting['value']) ? 1 : 0, - 'checked' => !empty($setting['value']) ? 'checked' : '', + (!empty($setting['value']) ? 'checked' : '') => !empty($setting['value']) ? 'checked' : '', 'id' => $settingId, 'data-setting-name' => $settingName, ]); @@ -421,26 +423,22 @@ function isLeaf($setting) }) $('.tab-content input').on('input', function() { - handleSettingValueChange($(this)) + if ($(this).attr('type') == 'checkbox') { + const $input = $(this) + const $inputGroup = $(this).closest('.form-group') + const settingName = $(this).data('setting-name') + const settingValue = $(this).is(':checked') + saveSetting($inputGroup[0], $input, settingName, settingValue) + } else { + handleSettingValueChange($(this)) + } }) $('.tab-content .input-group-actions .btn-save-setting').click(function() { const $input = $(this).closest('.input-group').find('input') const settingName = $input.data('setting-name') const settingValue = $input.val() - const url = '/instance/saveSetting/' - const data = { - name: settingName, - value: settingValue, - } - const APIOptions = { - statusNode: this - } - AJAXApi.quickFetchAndPostForm(url, data, APIOptions).then((result) => { - settingsFlattened[settingName] = result.data - $input.val(result.data.value) - handleSettingValueChange($input) - }) + saveSetting(this, $input, settingName, settingValue) }) $('.tab-content .input-group-actions .btn-reset-setting').click(function() { const $btn = $(this) @@ -451,6 +449,26 @@ function isLeaf($setting) }) }) + function saveSetting(statusNode, $input, settingName, settingValue) { + const url = '/instance/saveSetting/' + const data = { + name: settingName, + value: settingValue, + } + const APIOptions = { + statusNode: statusNode, + } + AJAXApi.quickFetchAndPostForm(url, data, APIOptions).then((result) => { + settingsFlattened[settingName] = result.data + if ($input.attr('type') == 'checkbox') { + $input.prop('checked', result.data.value) + } else { + $input.val(result.data.value) + } + handleSettingValueChange($input) + }) + } + function settingMatcher(params, data) { if (params.term == null || params.term.trim() === '') { return data; @@ -504,7 +522,8 @@ function isLeaf($setting) function handleSettingValueChange($input) { const oldValue = settingsFlattened[$input.data('setting-name')].value - if ($input.val() == oldValue) { + const newValue = ($input.attr('type') == 'checkbox' ? $input.is(':checked') : $input.val()) + if (newValue == oldValue) { restoreWarnings($input) } else { removeWarnings($input) @@ -532,6 +551,8 @@ function isLeaf($setting) $input.addClass('warning') } $inputGroup.parent().find('.invalid-feedback').addClass('d-block').text(setting.errorMessage) + } else { + removeWarnings($input) } const $callout = $input.closest('.settings-group') updateCalloutColors($callout) @@ -539,6 +560,9 @@ function isLeaf($setting) } function updateCalloutColors($callout) { + if ($callout.length == 0) { + return + } const $settings = $callout.find('input') const settingNames = Array.from($settings).map((i) => { return $(i).data('setting-name') diff --git a/webroot/css/bootstrap-additional.css b/webroot/css/bootstrap-additional.css index 00171be..63c730f 100644 --- a/webroot/css/bootstrap-additional.css +++ b/webroot/css/bootstrap-additional.css @@ -142,6 +142,22 @@ div.progress-timeline .progress-line.progress-inactive { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23ffc107' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e") } +.custom-control-input.is-invalid.warning ~ .custom-control-label { + color: unset; +} +.custom-control-input.is-invalid.warning ~ .custom-control-label::before { + border-color: #ffc107; +} +.custom-control-input.is-invalid.warning:checked ~ .custom-control-label::before { + background-color: #ffc107; +} +.custom-control-input.is-invalid.warning:focus:not(:checked) ~ .custom-control-label::before { + border-color: #ffc107; +} +.custom-control-input.is-invalid.warning:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem #ffc10740; +} + .p-abs-center-y { top: 50%; transform: translateY(-50%); diff --git a/webroot/js/bootstrap-helper.js b/webroot/js/bootstrap-helper.js index 80af3e8..61bca3f 100644 --- a/webroot/js/bootstrap-helper.js +++ b/webroot/js/bootstrap-helper.js @@ -833,7 +833,7 @@ class OverlayFactory { * @property {string=('primary'|'secondary'|'success'|'danger'|'warning'|'info'|'light'|'dark'|'white'|'transparent')} variant - The variant of the overlay * @property {number} opacity - The opacity of the overlay * @property {boolean} rounded - If the overlay should be rounded - * @property {number} auto - Whether overlay and spinner options should be adapted automatically based on the node + * @property {boolean} auto - Whether overlay and spinner options should be adapted automatically based on the node * @property {string=('primary'|'secondary'|'success'|'danger'|'warning'|'info'|'light'|'dark'|'white'|'transparent')} spinnerVariant - The variant of the spinner * @property {boolean} spinnerSmall - If the spinner inside the overlay should be small * @property {string=('border'|'grow')} spinnerSmall - If the spinner inside the overlay should be small